AI · Open source
AI SDK Bridge
Use Vercel AI SDK provider models (@ai-sdk/* and community providers, provider spec v2, v3 and v4) as TanStack AI 1.x adapters for chat, embeddings, transcription, speech, image generation and rerank.
What it solves
TanStack AI reaches a model through an adapter, and only some providers have one.
The Vercel AI SDK has provider packages for many more, from Vercel and the community, but they implement the AI SDK provider spec, not TanStack AI's adapter classes. A provider without a TanStack adapter cannot be used with chat() or embed(), and moving an AI SDK app to TanStack AI means replacing every provider package at the same time as the server route and the client.
Six functions wrap an AI SDK model object as a TanStack AI adapter: aiSdkText for chat(), aiSdkEmbedding for embed(), aiSdkTranscription for generateTranscription(), aiSdkSpeech for generateSpeech(), aiSdkImage for generateImage() and aiSdkRerank for rerank(). The adapter calls the model's own doStream, doGenerate, doEmbed or doRerank method, so the ai package is not needed; @ai-sdk/provider is installed as a dependency and used only for its types. It reads the model's specificationVersion when the adapter is created and throws for anything other than v2, v3 or v4 (v3 or v4 for reranking). Chat streams text, reasoning and tool calls as TanStack AI events, so chat() runs your tools in its agent loop; structured output makes one separate, non-streamed call with a JSON response format. Provider-specific options and headers pass through modelOptions unchanged. Tested with real provider packages against a local mock of each provider's API (no live APIs, no keys) for DeepSeek, Anthropic, OpenAI, Together AI, Cerebras, Fireworks, ollama-ai-provider-v2, openai-compatible and Cohere rerank, in a real browser in a TanStack Start 1.169 app, and from a fresh npm install. An independent package, not made or endorsed by TanStack or Vercel.
- aiSdkText: streaming chat with tool calls, tool choice, reasoning, structured output (outputSchema), token usage and abort
- aiSdkEmbedding, aiSdkTranscription, aiSdkSpeech, aiSdkImage and aiSdkRerank for the other AI SDK model kinds
- Reads AI SDK provider spec v2, v3 and v4 (AI SDK 5, 6 and 7 providers); reranking needs v3 or v4, and an unsupported spec throws when the adapter is created
- modelOptions.providerOptions and modelOptions.headers go to the AI SDK call unchanged on every adapter; chat sampling settings (temperature, topP, topK, penalties, seed, stopSequences, maxOutputTokens) pass through by name
- Usage mapped to TanStack token usage, including cached input, cache-write and reasoning tokens where the provider reports them
- Embeddings and images are split into batches by the model's maxEmbeddingsPerCall and maxImagesPerCall
- Provider errors in chat become a TanStack RUN_ERROR event; the other adapters log and rethrow the original error
- An optional { name } sets the adapter name shown by TanStack AI; it defaults to the provider id before the first dot
- Tested with real provider packages against a local mock of each provider's API, no live APIs and no keys
- ESM only, marked side-effect free; peer dependency @tanstack/ai ^1.0.0; Node 20 or later; 9.5 kB gzipped
Install
Requires TanStack AI 1.x.
npm install @amjadiqbal/tanstack-ai-sdk-bridge @tanstack/ai
Usage
// src/routes/api/chat.ts
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { aiSdkText } from '@amjadiqbal/tanstack-ai-sdk-bridge'
import { createDeepSeek } from '@ai-sdk/deepseek'
const deepseek = createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY })
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages, threadId } = await request.json()
const stream = chat({
adapter: aiSdkText(deepseek('deepseek-chat')),
messages,
threadId,
systemPrompts: ['You are a helpful assistant.'],
modelOptions: { temperature: 0.3, maxOutputTokens: 1024 },
})
return toServerSentEventsResponse(stream)
},
},
},
})
Setup
Install the bridge with @tanstack/ai, its only peer dependency (^1.0.0), then the AI SDK provider package you want to use. The bridge runs on the server, needs Node 20 or later and ships as an ES module only. Its one dependency, @ai-sdk/provider, supplies types; the built JavaScript imports only @tanstack/ai. The ai package itself is not needed. On the client you use TanStack AI as usual, for example useChat from @tanstack/ai-react; nothing from this package runs in the browser.
- 1 npm install @amjadiqbal/tanstack-ai-sdk-bridge @tanstack/ai
- 2 Install the AI SDK provider you want, for example npm install @ai-sdk/deepseek.
- 3 Create the provider as the AI SDK documents it, with its API key, base URL or custom fetch: createDeepSeek({ apiKey }).
- 4 Wrap a model and pass it as the adapter: chat({ adapter: aiSdkText(deepseek('deepseek-chat')), messages, threadId }).
Adapters and spec versions
Each function takes an AI SDK model object and an optional { name }, and returns a TanStack AI adapter. The name is what TanStack AI shows in devtools and usage; without it the bridge uses the provider id up to the first dot, so deepseek.chat becomes deepseek. The adapter reads specificationVersion from the model when you create it and throws "uses AI SDK spec ..., supported: ..." for any other version.
- chat(): aiSdkText(model), a LanguageModel, spec v2, v3 or v4.
- embed(): aiSdkEmbedding(model), an EmbeddingModel, spec v2, v3 or v4.
- generateTranscription(): aiSdkTranscription(model), a TranscriptionModel, spec v2, v3 or v4.
- generateSpeech(): aiSdkSpeech(model), a SpeechModel, spec v2, v3 or v4.
- generateImage(): aiSdkImage(model), an ImageModel, spec v2, v3 or v4.
- rerank(): aiSdkRerank(model), a RerankingModel, spec v3 or v4 (AI SDK 5 had no reranking models).
- The adapter classes (AiSdkTextAdapter and the others) are exported too, and each keeps the wrapped model and its specificationVersion as properties.
- Spec differences handled by the bridge: v2 and v3 file parts get raw data instead of the v4 wrapper, v2 flat usage counts and v3/v4 nested counts are both read, finish reasons are read as a string (v2) or an object (v3/v4), and the reasoning call option is set only for v4.
Chat
aiSdkText calls the model's doStream and turns AI SDK stream parts into TanStack AI events: text deltas into a text message, reasoning parts into a reasoning message, tool input and tool calls into tool call start, args and end. A response-metadata part updates the reported model id. Sources, files, raw chunks and provider-executed tool calls and results are not forwarded.
- System prompts become AI SDK system messages. User text, image, audio, video and document parts become text and file parts; a part without a MIME type gets image/*, audio/*, video/* or application/pdf. URL sources stay URLs, data sources stay data.
- Provider file handles (TanStack file sources) are rejected with TanStack's unsupported-file-source error.
- Earlier assistant turns are sent back with their text, tool calls and non-redacted thinking as reasoning text. Tool results are sent as text, or as error text when the tool failed. Tool call ids are rewritten to letters, digits, _ and - (at most 64 characters) so they replay safely across providers.
- modelOptions: maxOutputTokens, temperature, topP, topK, presencePenalty, frequencyPenalty, stopSequences and seed are typed; every key except providerOptions and headers is passed to the AI SDK call under the same name.
- modelOptions.providerOptions goes through unchanged, keyed by provider, for example { deepseek: { ... } }. modelOptions.headers sets extra HTTP headers.
- Abort: the abortController signal from chat(), or the request signal, is passed to the model as abortSignal, and the bridge also cancels the stream when it fires.
- Finish reason: tool_calls when the model called any of your tools, otherwise length, content_filter or stop.
- Usage: input, output and total tokens, plus cached input tokens, cache-write tokens (v3/v4) and reasoning tokens when the provider reports them.
- Errors: an error thrown by the provider, or an error part in its stream, ends the run with a RUN_ERROR event carrying the message and code. If the stream ends without a finish part, open messages are closed and the run finishes with stop or tool_calls.
Tools and tool choice
Tools are ordinary TanStack AI tools. The bridge sends each one to the model as an AI SDK function tool with its name, description and JSON Schema input (an empty object schema when a tool has none), streams the calls back, and chat() runs the tools and continues the conversation. Tool call arguments are streamed as they arrive when the provider sends input deltas, and sent in one piece when it does not.
- toolChoice from chat() is passed on when tools are present: a string choice keeps its value as the AI SDK type, and a named tool becomes { type: 'tool', toolName }.
- Provider-executed tools, such as a provider's own web search or code execution, are not forwarded; only function tools are.
import { chat, toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
const getWeather = toolDefinition({
name: 'getWeather',
description: 'Get the weather for a city',
inputSchema: z.object({ city: z.string() }),
}).server(async ({ city }) => ({ city, tempC: 21 }))
chat({ adapter: aiSdkText(deepseek('deepseek-chat')), messages, threadId, tools: [getWeather] })
Structured output
With outputSchema, the bridge makes one non-streamed doGenerate call with the AI SDK response format { type: 'json', schema } and without tools. It joins the text parts of the answer and parses them as JSON, also when the model wrapped the JSON in a Markdown code fence, and returns the data, the raw text, usage, and the response id and model when the provider gives them. Checking the data against your schema is left to TanStack AI.
- If the answer was cut off at the token limit, it throws and tells you to raise modelOptions.maxOutputTokens.
- If the answer is not valid JSON, it throws with the first 200 characters of what the model returned.
- Every error is rethrown as "Structured output generation failed: ..." with the original error as its cause.
const product = await chat({
adapter: aiSdkText(deepseek('deepseek-chat')),
messages: [{ role: 'user', content: 'Invent a product.' }],
threadId: 'product-1',
outputSchema: z.object({ name: z.string(), price: z.number() }),
stream: false,
})
Reasoning
chat({ reasoning }) sets the AI SDK reasoning call option on spec v4 (AI SDK 7) models only. The levels minimal, low, medium, high, xhigh and max keep their names and off becomes none; a token budget is not supported. On v2 and v3 models the level is not sent, so set reasoning through modelOptions.providerOptions the way that provider documents it. Reasoning from any spec version streams as TanStack reasoning events and closes before the answer text or a tool call starts.
- Reasoning is sent back in later turns as plain text, without signatures. Providers that need signed thinking blocks in later turns, such as Anthropic extended thinking with tools, receive it as plain text or drop it.
Embeddings
aiSdkEmbedding accepts text input only and returns one vector per input with its index. Inputs are sent in batches of the model's maxEmbeddingsPerCall, or in one call when the model sets no limit. Token usage is summed over the batches and reported as prompt tokens.
- dimensions is sent to spec v4 models. On a v2 or v3 model, setting dimensions throws before any request; pass it in modelOptions.providerOptions instead.
- providerOptions and headers pass through. There is no abort: TanStack AI 1.0 gives embedding adapters no abort signal, and the bridge sends none.
import { embed } from '@tanstack/ai'
import { aiSdkEmbedding } from '@amjadiqbal/tanstack-ai-sdk-bridge'
import { openai } from '@ai-sdk/openai'
const { embeddings } = await embed({
adapter: aiSdkEmbedding(openai.embedding('text-embedding-3-small')),
input: ['red guitar', 'blue drum kit'],
})
Transcription, speech, image and rerank
These four adapters forward TanStack AI's abort signal, modelOptions.providerOptions and modelOptions.headers to the AI SDK call. On failure they log the error through TanStack AI's logger and rethrow it unchanged.
- Transcription: audio can be a File, Blob, ArrayBuffer, base64 string, data URL or http(s) URL. A URL is downloaded by the bridge with the global fetch, not the provider's fetch. The media type comes from the file, data URL or response header, else modelOptions.mediaType (default audio/mpeg). The result has the text and, when the model returns them, language, duration and segments with start, end and text. TanStack's language, prompt and responseFormat options are not sent; pass them in modelOptions.providerOptions. No usage is reported.
- Speech: text, voice, format (as the AI SDK outputFormat) and speed are passed on, plus modelOptions.instructions and modelOptions.language. Audio comes back as base64. The result's format comes from the provider's content-type header, else from the first bytes of the audio (MP3, WAV, Ogg, FLAC or ADTS AAC; Ogg is reported as opus), else the format you asked for, else unknown. contentType is the header's media type, else the type for that format, else application/octet-stream. Multi-voice turns and timestamps are not supported. No usage is reported.
- Image: text parts of the prompt are joined into the prompt and image parts become input files, by URL or data; any other part type throws, and image inputs on a spec v2 model throw. size is passed on, and modelOptions takes aspectRatio, seed and mask. numberOfImages is split into calls of maxImagesPerCall (1 when the model does not say) and stops early if a call returns no images. Images come back as base64. modelOptions.mask, an AI SDK image file ({ type: 'file', mediaType, data } or { type: 'url', url }), is sent as the mask, and a mask on a spec v2 model throws. Image parts in the prompt are always sent as input files, never as the mask. Usage is summed when the provider reports it.
- Rerank: query, the documents as strings and topN are passed on; the result maps each relevanceScore to score with its index. AI SDK reranking models report no token usage, so usage is always zero.
import { generateImage, generateSpeech, generateTranscription, rerank } from '@tanstack/ai'
import { aiSdkImage, aiSdkRerank, aiSdkSpeech, aiSdkTranscription } from '@amjadiqbal/tanstack-ai-sdk-bridge'
import { openai } from '@ai-sdk/openai'
import { cohere } from '@ai-sdk/cohere'
const threadId = 'media-1'
await generateTranscription({ threadId, adapter: aiSdkTranscription(openai.transcription('whisper-1')), audio: file })
await generateSpeech({ threadId, adapter: aiSdkSpeech(openai.speech('gpt-4o-mini-tts')), text: 'Hello', voice: 'alloy' })
await generateImage({ threadId, adapter: aiSdkImage(openai.image('gpt-image-1')), prompt: 'a lighthouse at dusk' })
await rerank({ adapter: aiSdkRerank(cohere.reranking('rerank-v3.5')), query: 'refund policy', documents })
What is not supported
These are gaps in 0.1.1, read from the code at the tag.
- Provider-executed tools are not forwarded, and their results, sources and generated files in a chat stream are dropped.
- Reasoning signatures are not sent back in later turns.
- chat({ wrapFetch }) is not applied; pass a custom fetch to the provider factory, for example createDeepSeek({ fetch }).
- Provider file handles (TanStack file sources) are rejected in chat and image prompts; send media as data or URLs.
- The reasoning level is only sent to spec v4 models, and a reasoning token budget is not supported.
- dimensions on a v2 or v3 embedding model throws. Embeddings cannot be aborted.
- Transcription language, prompt and responseFormat; speech turns and timestamps.
- No token usage for transcription or speech, and always zero for rerank.
- Video generation, audio generation and realtime models are not bridged.
Moving an AI SDK app to TanStack AI
You keep your provider packages and their setup, and move the server route and the client to TanStack AI. Later you can swap a bridged provider for a native TanStack adapter if one exists and you need its per-model options.
- streamText({ model: deepseek('deepseek-chat'), ... }) becomes chat({ adapter: aiSdkText(deepseek('deepseek-chat')), ... }).
- generateText(...) becomes chat({ ..., stream: false }); generateObject({ schema }) becomes chat({ ..., outputSchema }).
- embedMany({ model: openai.embedding(...) }) becomes embed({ adapter: aiSdkEmbedding(openai.embedding(...)) }).
- transcribe, generateSpeech, generateImage and rerank become TanStack's generateTranscription, generateSpeech, generateImage and rerank with the matching aiSdk* adapter.
- system: '...' becomes systemPrompts: ['...']; temperature, maxOutputTokens and providerOptions move into modelOptions.
- tool({ inputSchema, execute }) becomes toolDefinition({ inputSchema }).server(execute).
- useChat from @ai-sdk/react becomes useChat from @tanstack/ai-react, and result.toUIMessageStreamResponse() becomes toServerSentEventsResponse(stream).
Compatibility and testing
The bridge needs @tanstack/ai ^1.0.0 and was tested with 1.0.0. Besides the main entry it imports helpers (transformMessagesForReplay, tanstackMetadata, toRunErrorPayload) from the @tanstack/ai/adapter-internals subpath, so a later 1.x release that changes that subpath could break it. It was tested with real provider packages against a local mock of each provider's API (no live APIs, no keys) for: deepseek, anthropic, openai, togetherai, cerebras, fireworks, ollama-ai-provider-v2, openai-compatible, and cohere rerank. The mock speaks each provider's wire format, so these tests cover request and stream formats, not every model's behaviour. It was also tested in a real browser in a TanStack Start 1.169 app with @tanstack/ai-react, and from a fresh install from npm. Other providers have not been tested. An independent package, not made or endorsed by TanStack or Vercel; AI SDK and provider names are used only to say what it works with.
- Chat stream, tool loop and structured output: @ai-sdk/deepseek, @ai-sdk/anthropic, @ai-sdk/openai (Responses and Chat Completions), @ai-sdk/togetherai, @ai-sdk/cerebras, @ai-sdk/fireworks, ollama-ai-provider-v2, and @ai-sdk/openai-compatible 3.x, 2.x and 1.x, which cover spec v4, v3 and v2.
- Reasoning: @ai-sdk/deepseek and @ai-sdk/anthropic. Embeddings, image, speech and transcription: @ai-sdk/openai. Rerank: @ai-sdk/cohere.
- Browser test in TanStack Start 1.169 with @tanstack/ai-react useChat and a /api/chat server route, using the packed npm tarball.
- Unit tests run every adapter through the real chat(), embed(), generateTranscription(), generateSpeech(), generateImage() and rerank() against AI SDK mock models and hand-written spec v2 models, covering streaming, tool calls, tool choice, reasoning levels, errors, abort, structured output, batching, file parts for v2, v3 and v4, the reported speech format and the image mask.
- Not tested: live provider APIs, other AI SDK providers, Cohere chat and embeddings and Ollama embeddings (the mock's responses for those fail the providers' own validation), and Vue, Solid or Svelte clients.
Hire me
Need this built into your app, or something custom?
I wrote AI SDK Bridge. I can wire it into your TanStack app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More TanStack packages
All packages
Query Key Kit
Declare your TanStack Query v5 keys once as a typed tree, get queryOptions()-shaped objects back, and invalidate or update whole branches of the cache with typed helpers.
Start SEO Kit
Typed SEO helpers for TanStack Start: one head() call for title, canonical, Open Graph, Twitter, robots and hreflang tags, a sitemap and robots.txt served from server routes, typed JSON-LD with Product schema, and an optional Open Graph image route.
Table URL State
Keep a TanStack Table v9 table's sorting, column filters, global filter, pagination and column visibility in the URL search params, in both directions, and export the current view to CSV or, from a separate entry point, to XLSX.
shopify-app-tanstack-start
Build embedded Shopify apps on TanStack Start with the server API of Shopify's React Router package: session tokens and token exchange, webhook and app proxy verification, the mandatory compliance webhooks and the Billing API, as Start middleware, server functions and server routes.