Skip to content

Use the Vercel AI SDK with Router One: OpenAI, OpenAI-compatible and Anthropic providers

The Vercel AI SDK (the ai package) calls models through provider packages, and three of them can point at Router One with a baseURL and a key: @ai-sdk/openai, @ai-sdk/openai-compatible and @ai-sdk/anthropic. The provider you pick, and the method you call on it, decide the endpoint. routerOne.chat(id) sends Chat Completions, which serves every chat model in the catalog, while the OpenAI provider's default call, routerOne(id), sends the Responses API, which Router One serves only for GPT-family, DeepSeek and Grok chat IDs. generateText, streamText and tool loops run in your server code; the gateway serves each model request and records a cost and latency trace for it. This guide covers AI SDK 7's install requirements, the three providers side by side, the plain-string trap that sends catalog IDs to Vercel's own AI Gateway, streaming usage and reasoning text, Claude over /v1/messages, the AI SDK 7 changes that break older examples, retries, and the error a failed long call produces. Checked against ai 7.0.136, @ai-sdk/openai 4.0.91, @ai-sdk/openai-compatible 3.0.67 and @ai-sdk/anthropic 4.0.78 on 2026-10-09, per the AI SDK docs and package source.

Install AI SDK 7 and keep the key on the server

npm install ai @ai-sdk/openai installs the core and the OpenAI provider; add @ai-sdk/openai-compatible or @ai-sdk/anthropic if you use the providers described below. AI SDK 7 requires Node.js 22 or later and is ESM-only, so require() no longer loads it: use import in an ES module, with "type": "module" in package.json or an .mjs or .mts file. In Next.js, call the SDK from route handlers, server actions or other server code, and keep the key in a server-side variable; a NEXT_PUBLIC_ prefix would ship it in the browser bundle. Next.js loads .env.local by itself, and a plain Node.js script can use node --env-file=.env.local. When baseURL and apiKey are not passed, createOpenAI falls back to OPENAI_BASE_URL and OPENAI_API_KEY, and createAnthropic to ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY. The examples pass both values explicitly and read the key from ROUTER_ONE_API_KEY, a name this guide chooses, so variables set for other tools cannot redirect them. Create a dedicated key for the app in the dashboard and give it a maxSpend cap.

.env.local
# Server-side only: never prefix it with NEXT_PUBLIC_
ROUTER_ONE_API_KEY=sk-your-router-one-key

Configure Vercel AI SDK to use the Router One base URL

Save this as ai-sdk-example.ts in a project whose package.json sets "type": "module", then run it with node ai-sdk-example.ts on Node.js 22.18 or later, which runs TypeScript files directly, or with a runner such as tsx. createOpenAI takes the /v1 base URL, and the provider appends the endpoint path to it. routerOne.chat(id) sends POST /v1/chat/completions, which serves every chat model in the catalog, so the same file works with anthropic/claude-sonnet-5, deepseek-v4.1-flash, google/gemini-3.7-flash or grok-4.7. Do not shorten it to routerOne(id): since AI SDK 5 the OpenAI provider's default call uses the Responses API, and a Claude-family ID sent there is rejected with HTTP 400 before any model is called. instructions is AI SDK 7's name for the system prompt, and .chat() sends it as a system message. maxOutputTokens goes out as max_tokens, because the provider does not treat vendor-prefixed IDs as OpenAI reasoning models; Router One's Chat Completions accepts it as the output cap. streamText on .chat() always asks for include_usage, so await result.usage resolves to the token counts once the stream ends; in AI SDK 7, usage on a result totals every step of a tool loop.

ai-sdk-example.ts
import { createOpenAI } from "@ai-sdk/openai";
import { generateText, streamText } from "ai";

const routerOne = createOpenAI({
  baseURL: "https://api.router.one/v1",
  apiKey: process.env.ROUTER_ONE_API_KEY,
});

// .chat(id) sends POST /v1/chat/completions: every chat model in the catalog
const { text, usage } = await generateText({
  model: routerOne.chat("anthropic/claude-sonnet-5"),
  instructions: "Answer in one sentence.",
  prompt: "What does an LLM API gateway do?",
  maxOutputTokens: 512,
});
console.log(text, usage);

const result = streamText({
  model: routerOne.chat("deepseek-v4.1-flash"),
  prompt: "Count to five.",
});
for await (const delta of result.textStream) process.stdout.write(delta);
console.log(await result.usage);

Which provider call sends which request

Three provider packages can reach Router One, and for the OpenAI provider the method matters as much as the package. Match each call in your code to a row:

Which provider call sends which request
Provider callRouter One endpointWhat to know
createOpenAI({ baseURL, apiKey }).chat(id)POST /v1/chat/completionsEvery chat model; streaming always requests usage; the non-standard reasoning_content field is not surfaced
createOpenAI(...)(id), .languageModel(id) or .responses(id)POST /v1/responsesOnly IDs whose model page lists this endpoint (GPT-family, DeepSeek and Grok chat IDs); a Claude ID gets HTTP 400 must be called via /v1/messages or /v1/chat/completions
createOpenAICompatible({ name, baseURL, apiKey })(id)POST /v1/chat/completionsEvery chat model; reasoning_content becomes reasoningText; turn on includeUsage and supportsStructuredOutputs yourself
createAnthropic({ baseURL: 'https://api.router.one/v1', apiKey })(id)POST /v1/messagesClaude-family and DeepSeek IDs; the baseURL must end in /v1
A plain string, such as model: 'anthropic/claude-sonnet-5'Vercel's AI Gateway, not Router OneResolved through globalThis.AI_SDK_DEFAULT_PROVIDER, which falls back to Vercel's gateway provider
embed() or embedMany() with routerOne.embedding(id)No route (HTTP 404)Keep embeddings on a provider that serves them

The plain-string trap: catalog IDs look like AI Gateway model strings

AI SDK 7 accepts a plain string as model and resolves it through globalThis.AI_SDK_DEFAULT_PROVIDER, falling back to Vercel's AI Gateway provider when nothing is registered. Router One catalog IDs such as anthropic/claude-sonnet-5 and openai/gpt-5.5 have the same provider/model shape as AI Gateway strings, so generateText({ model: "anthropic/claude-sonnet-5" }) never reaches Router One. It goes to Vercel's gateway instead: without AI_GATEWAY_API_KEY it fails with Unauthenticated request to AI Gateway, with one it runs on your Vercel AI Gateway account, and in both cases nothing appears in Dashboard → Logs. Pass a provider model object, as the other examples do, or register a default provider once at startup, and choose that default with care. With a createOpenAI provider as the default, a plain string resolves through routerOne(id), which is the Responses API, so strings then work only for Responses IDs. With a createOpenAICompatible provider as the default, as below, plain strings go to /v1/chat/completions and work for every chat model.

default-provider.ts
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText } from "ai";

globalThis.AI_SDK_DEFAULT_PROVIDER = createOpenAICompatible({
  name: "router-one",
  baseURL: "https://api.router.one/v1",
  apiKey: process.env.ROUTER_ONE_API_KEY,
  includeUsage: true,
});

// Plain strings now resolve through Router One's Chat Completions endpoint
const { text } = await generateText({
  model: "anthropic/claude-sonnet-5",
  prompt: "Hello!",
});

@ai-sdk/openai-compatible: reasoning text and explicit usage

createOpenAICompatible builds a provider that only speaks Chat Completions. name and baseURL are required and neither is read from the environment; apiKey is sent as a Bearer token. Two settings default to off and are worth turning on for Router One. includeUsage: true adds stream_options.include_usage to streaming requests; without it a streamed result has no usage. supportsStructuredOutputs: true lets Output.object send response_format json_schema, with the name response; without it the provider falls back to json_object and warns that the schema was not sent. For Claude-family IDs, response_format on Chat Completions is not a reliable way to get JSON at all; the structured outputs guide shows the /v1/messages route. This provider also reads the non-standard reasoning_content field: thinking from IDs such as anthropic/claude-sonnet-4.6-thinking arrives as reasoningText on the result and as reasoning deltas on a stream, which the OpenAI provider's .chat() does not do. maxOutputTokens is sent as max_tokens.

compatible.ts
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText } from "ai";

const routerOne = createOpenAICompatible({
  name: "router-one",
  baseURL: "https://api.router.one/v1",
  apiKey: process.env.ROUTER_ONE_API_KEY,
  includeUsage: true,
  supportsStructuredOutputs: true,
});

const { text, reasoningText, usage } = await generateText({
  model: routerOne("anthropic/claude-sonnet-4.6-thinking"),
  prompt: "Is 221 prime?",
  maxOutputTokens: 2048,
});
console.log(reasoningText, text, usage);

@ai-sdk/anthropic for Claude and DeepSeek on /v1/messages

@ai-sdk/anthropic speaks Anthropic Messages, which Router One serves for the currently listed Claude-family models and DeepSeek IDs. Unlike Anthropic's own SDK, this provider appends only /messages to baseURL and adds /v1 by itself only for api.anthropic.com, so the baseURL must be https://api.router.one/v1. With the host root it posts to https://api.router.one/messages and gets HTTP 404, and an ANTHROPIC_BASE_URL exported for Claude Code holds exactly that host root, so pass baseURL explicitly. apiKey is sent as an x-api-key header, which the gateway accepts; authToken is sent as a Bearer token; passing both throws an error. The provider reads ANTHROPIC_API_KEY from the environment but not ANTHROPIC_AUTH_TOKEN, so the token variable of a Claude Code setup is not picked up. Set maxOutputTokens on every call. The provider looks up per-model defaults by hyphenated Anthropic model names, and Router One's dotted catalog IDs (anthropic/claude-sonnet-4.6, anthropic/claude-opus-4.8, and the aws/ and vertex/ IDs) fall into its generic Claude entry: max_tokens 128000, with temperature, topP and topK removed from the request and a warning in result.warnings. Names it does match, such as anthropic/claude-sonnet-5, have the sampling parameters removed as well, and DeepSeek IDs get a 4096-token default. Anthropic server tools work where the gateway accepts them: web search and code execution are accepted and metered, while web fetch and MCP servers are rejected with HTTP 400.

anthropic.ts
import { createAnthropic } from "@ai-sdk/anthropic";
import { generateText } from "ai";

const routerOneMessages = createAnthropic({
  baseURL: "https://api.router.one/v1", // this provider appends /messages
  apiKey: process.env.ROUTER_ONE_API_KEY,
});

const { text } = await generateText({
  model: routerOneMessages("anthropic/claude-sonnet-5"),
  prompt: "Hello!",
  maxOutputTokens: 1024,
});
console.log(text);

AI SDK 7 changes that break older examples

Code written for AI SDK 5 or 6 needs a few edits before it runs on 7. system is deprecated in favor of instructions, and when both are set, instructions wins. System messages inside prompt or messages are now rejected by default with System messages are not allowed in the prompt or messages fields. Use the instructions option instead. Move them to instructions, or pass allowSystemInMessages: true only for histories your own server wrote, because a system message supplied by a user could override your prompt. usage on generateText and streamText results now totals every step of a tool loop, which is what totalUsage used to hold, so compare it with the sum of that loop's rows in Dashboard → Logs and read per-step figures from steps. stepCountIs is renamed isStepCount; generateText stops after one step unless stopWhen allows more, and every step is one model request. The packages are ESM-only and need Node.js 22 or later. The official codemods, such as npx @ai-sdk/codemod v7/rename-system-to-instructions, handle most of the renames.

Retries, tool loops and the long-call error

generateText and streamText retry a failed call twice by default (maxRetries), on top of the gateway's own failover: requests are served by the model you name, and a route that returns 429, 5xx or times out is retried on another route for the same model before the gateway answers. In a tool loop every step is a separate request, and every client attempt the gateway forwards to a model has its own request_id in Dashboard → Logs. Bound loops with stopWhen, lower maxRetries for batch work, and give the app a key with maxSpend so a runaway loop stops with HTTP 402 at your cap. A non-streaming call that runs past the gateway's keepalive point, about 25 seconds, and then fails returns HTTP 200 with a top-level error object. Both OpenAI providers then throw APICallError with the message Invalid JSON response, and the gateway's own error text is in the error's responseBody. streamText avoids this, so prefer it for long generations and reasoning models.

Which model ID should Vercel AI SDK send?

Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in Vercel AI SDK. A catalog listing does not mean the client can use every feature of that model. Give each client or application a dedicated API key with a maxSpend cap.

Which API protocol is Vercel AI SDK using?

OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.

Verify the Vercel AI SDK call in your request trace

Send a simple text request from Vercel AI SDK, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, either the request never reached the gateway (client configuration or connectivity) or the gateway refused it before calling a model — a 401 for the key, a 402 for funds, a 429 from your key's or account's limits, or a 400 or 404 for the model ID or endpoint; a streamed request that failed before any output may have no log either. The status and error message the client received tell which, so check them before attributing the error to the gateway or upstream.

FAQ

Why does routerOne('<id>') return 400 must be called via…?

Because routerOne(id) on a createOpenAI provider uses the Responses API, the OpenAI provider's default since AI SDK 5, and Router One serves /v1/responses only for GPT-family, DeepSeek and Grok chat IDs. A Claude-family ID sent there is rejected before any model is called with model '<id>' must be called via /v1/messages or /v1/chat/completions. Call routerOne.chat(id) instead, which uses /v1/chat/completions for every chat model, or switch to @ai-sdk/openai-compatible, whose default call is Chat Completions. Keep routerOne.responses(id) for IDs whose model page lists POST /v1/responses, when you need Responses features such as previous_response_id.

Should I use .chat() or @ai-sdk/openai-compatible?

Both send Chat Completions to every chat model. Use createOpenAI(...).chat(id) when you also want the Responses API from the same provider for GPT-family IDs, or OpenAI-specific provider options; it always requests streaming usage. Use createOpenAICompatible when you want thinking text, since it surfaces reasoning_content as reasoningText and .chat() does not. It needs includeUsage: true for streamed usage and supportsStructuredOutputs: true for json_schema output, both off by default, and it is the safer global default provider for plain-string model IDs.

Can I pass the catalog ID as a plain string?

Only after registering a Router One provider as globalThis.AI_SDK_DEFAULT_PROVIDER. Without it, a string such as anthropic/claude-sonnet-5 goes to Vercel's AI Gateway, never to Router One, and does not appear in Dashboard → Logs. Register a createOpenAICompatible provider so strings go to /v1/chat/completions; a createOpenAI provider as the default would send them to /v1/responses, where only GPT-family, DeepSeek and Grok chat IDs work.

Why do I get System messages are not allowed after upgrading to AI SDK 7?

AI SDK 7 rejects system messages inside prompt or messages by default, before any request is sent, so nothing reaches the gateway. Put the system prompt in instructions, or pass allowSystemInMessages: true for chat histories that your own server wrote and that already contain system messages.

Where is Claude's thinking?

The OpenAI provider's .chat() does not read the non-standard reasoning_content field, so thinking from the -thinking IDs is generated and billed as output but not surfaced. Use @ai-sdk/openai-compatible, which returns it as reasoningText over Chat Completions, or @ai-sdk/anthropic over /v1/messages, which returns thinking as reasoning parts.

Do embeddings work through Router One?

No. Router One serves no /v1/embeddings, so embed() and embedMany() with a Router One provider get HTTP 404. Keep the embedding model on a provider that offers embeddings, with that provider's key, and send only chat calls to the gateway.

Which models can Vercel AI SDK use through the gateway?

Choose a current catalog model that supports both the endpoint and the features Vercel AI SDK uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. A client that lists a model has only read the ID, from its own configuration or from GET /v1/models; verify an actual request too.

Models are listed, but requests fail with 400 or 404. What should I check?

Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.

Does this work from Mainland China?

Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.

How do I debug a 401/402/403/429?

Start from what your client received: the status, the error body and the X-Request-ID header. A 401 (missing, unknown, revoked or expired key), a 402 (wallet balance, or the key's maxSpend cap) and a 429 from your key's or account's rate limits are refused before any model is called, so they never appear in Dashboard → Logs — nor does a 400 or 404 for a wrong model ID or endpoint. For 401, check that the full key is sent and still active in Dashboard → API Keys; for 402, top up or raise the key's maxSpend; for a 403, keep the full error body. For 429, back off and retry later; a 429 that does show up in Logs came back from upstream after the gateway's retries. Keep the request_id and follow the error-codes reference.