本页还没有译文
本页 115 段还没译成日本語,下面显示的是英文原文。OpenRouter Adapter
还没翻译译自英文(tanstack.com)本页 115 段都还没译
OpenRouter is TanStack AI's first official AI partner and the recommended starting point for most projects. It provides access to 300+ models from OpenAI, Anthropic, Google, Meta, Mistral, and many more — all through a single API key and unified interface.
Installation
Basic Usage
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages: [{ role: "user", content: "Hello!" }],
});Configuration
import { createOpenRouterText } from "@tanstack/ai-openrouter";
const adapter = createOpenRouterText(
"openai/gpt-5",
process.env.OPENROUTER_API_KEY!,
{
serverURL: "https://openrouter.ai/api/v1", // Optional
httpReferer: "https://your-app.com", // Optional, for rankings
appTitle: "Your App Name", // Optional, for rankings
},
);Need a header on each request, or a log of each model call? Both text adapters support wrapFetch. If you set an httpClient, each wrapped request still goes through it.
Retry rate limits
A busy upstream provider can reply with HTTP 429. The adapter retries only 5XX errors by default. Add "429" to retryCodes to retry rate limits too:
import { createOpenRouterText } from "@tanstack/ai-openrouter";
const adapter = createOpenRouterText(
"openai/gpt-5",
process.env.OPENROUTER_API_KEY!,
{
retryConfig: {
strategy: "backoff",
backoff: {
initialInterval: 500,
maxInterval: 60000,
exponent: 1.5,
maxElapsedTime: 120000,
},
retryConnectionErrors: true,
},
retryCodes: ["429", "5XX"],
},
);retryCodes: the HTTP status codes to retry."5XX"matches every 5xx code.retryConfig: how long to wait between tries. When the response has aRetry-Afterheader, the SDK waits that long.
Now a 429 waits, then retries, and the chat continues with the next response.
Retry an error in the stream
Most provider errors come as an HTTP status, and the SDK retries them as shown above. But OpenRouter can send status 200 first. Then a provider error comes as an error chunk in the stream, and the SDK cannot retry it.
To retry that error, pass retry to chat():
import { chat } from "@tanstack/ai";
import { createOpenRouterText } from "@tanstack/ai-openrouter";
const adapter = createOpenRouterText(
"openai/gpt-5.5",
process.env.OPENROUTER_API_KEY!,
{ retryCodes: ["429", "5XX"] },
);
const stream = chat({
adapter,
messages: [{ role: "user", content: "Hello!" }],
retry: { maxRetries: 3 },
});When chat() retries:
- Only for an error code in
retryCodes. The default is 5XX only, so add"429"to retry a rate limit. - Only when the call streamed no output yet. An error after part of the answer reaches you as a
RUN_ERROR. - Not when
retryConfighasstrategy: "none". - Only with the Chat Completions adapter (
openRouterText). The Responses adapter does not do this.
The wait is the initialInterval of retryConfig (default 500 ms). It is the same for each try and does not grow. See Let chat() retry for you.
Available Models
OpenRouter provides access to 300+ models from various providers. Models use the format provider/model-name:
model: "openai/gpt-5.1"
model: "anthropic/claude-sonnet-4.5"
model: "google/gemini-3.1-pro-preview"
model: "meta-llama/llama-4-maverick"
model: "deepseek/deepseek-v3.2"See the full list at openrouter.ai/models.
adapter.inputModalities lists the input kinds of the selected model. It is undefined for a model that this package does not know. See Check what a model accepts.
Example: Chat Completion
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages,
});
return toServerSentEventsResponse(stream);
}Example: With Tools
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
import { z } from "zod";
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather",
inputSchema: z.object({
location: z.string(),
}),
});
const getWeather = getWeatherDef.server(async ({ location }) => {
return { temperature: 72, conditions: "sunny" };
});
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages,
tools: [getWeather],
});
return toServerSentEventsResponse(stream);
}Tools and structured output together
You can pass both tools and outputSchema on one chat() call. For some
upstream models OpenRouter can return the typed object in that same streaming
request, so the engine does not make a second finalization call.
That happens only when every model that can receive the request is in
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS. The set is generated from
OpenRouter's catalog on every model sync: every chat model whose
supported_parameters include structured_outputs, tools and tool_choice
(Claude 4.5+, Gemini 2.5+, GPT-4o+, Grok 4, DeepSeek V3+, Llama 3.1+, and so
on). Models OpenRouter does not flag, such as anthropic/claude-opus-4.1, stay
on the legacy two-call path.
If any fallback in modelOptions.models is outside that set, OpenRouter keeps
the two-call path. Routing suffixes such as :nitro do not change the gate.
Import the set from @tanstack/ai-openrouter/model-meta if you need to check a
model before you send:
import { OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS } from "@tanstack/ai-openrouter/model-meta";
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS.has("openai/gpt-5.5");Chat Completions (openRouterText) and Responses (openRouterResponsesText)
both attach the schema on this path. The client does not change: useChat({ outputSchema }) still reads partial and final.
Server (Chat Completions):
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
import { z } from "zod";
const getWeather = toolDefinition({
name: "get_weather",
description: "Get the current weather",
inputSchema: z.object({ location: z.string() }),
}).server(async ({ location }) => {
return { temperature: 72, conditions: "sunny", location };
});
const AnswerSchema = z.object({
summary: z.string(),
location: z.string(),
});
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openRouterText("openai/gpt-5.5"),
messages,
tools: [getWeather],
outputSchema: AnswerSchema,
stream: true,
});
return toServerSentEventsResponse(stream);
}Server (Responses). Same tools and outputSchema as the Chat Completions
example, with openRouterResponsesText:
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { openRouterResponsesText } from "@tanstack/ai-openrouter";
import { z } from "zod";
const getWeather = toolDefinition({
name: "get_weather",
description: "Get the current weather",
inputSchema: z.object({ location: z.string() }),
}).server(async ({ location }) => {
return { temperature: 72, conditions: "sunny", location };
});
const AnswerSchema = z.object({
summary: z.string(),
location: z.string(),
});
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openRouterResponsesText("openai/gpt-5.5"),
messages,
tools: [getWeather],
outputSchema: AnswerSchema,
stream: true,
});
return toServerSentEventsResponse(stream);
}Client:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { z } from "zod";
const AnswerSchema = z.object({
summary: z.string(),
location: z.string(),
});
const { sendMessage, partial, final } = useChat({
connection: fetchServerSentEvents("/api/chat"),
outputSchema: AnswerSchema,
});See Structured Outputs with tools for the
event order, and Middleware for how
structuredOutput phase behaves on this path.
To try this in a browser, run examples/ts-react-chat and open
/generations/openrouter-combined. The page shows the tool call, the typed
object, and the adapter call counts. structuredOutputStream must stay at 0.
Environment Variables
Set your API key in environment variables:
OPENROUTER_API_KEY=sk-or-...Model Routing
OpenRouter can automatically route requests to the best available provider:
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openRouterText("openrouter/auto"),
messages,
modelOptions: {
models: [
"openai/gpt-5.5",
"anthropic/claude-sonnet-4.5",
"google/gemini-3.1-pro-preview",
],
},
});
return toServerSentEventsResponse(stream);
}models is the fallback list: OpenRouter tries them in order when the primary model is unavailable. The rest of OpenRouter's routing surface is also exposed through modelOptions and forwarded to the request as-is. The option names are the camelCase ones from the OpenRouter SDK.
Provider preferences
provider controls which upstream providers may serve the request and in what order. Pin one provider and disable fallbacks:
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterText("meta-llama/llama-4-maverick"),
messages: [{ role: "user", content: "Hello!" }],
modelOptions: {
provider: {
order: ["groq", "together"], // provider slugs, in priority order
allowFallbacks: false, // fail instead of routing elsewhere
},
},
});Provider slugs are the lowercase ids shown on OpenRouter's model pages ("anthropic", "openai", "groq", …). Other fields on provider:
only/ignore: allow-list or deny-list of provider slugs, merged with your account-wide provider settings for this request.requireParameters: only route to providers that support every parameter in the request. Without it, OpenRouter sends each provider only the parameters it supports and silently drops the rest, so aresponseFormator a sampling option can be ignored without an error. When you stream withrequireParameters: true, also setmodelOptions.streamOptions: { includeUsage: false }. Without it, the adapter sendsstream_options, and OpenRouter returns a 404 for providers that do not support that parameter.sort:"price","throughput", or"latency", or an object withbyandpartition, applied whenorderis not set. Setting it disables load balancing.maxPrice: the highest pricing you accept. It takespromptandcompletionin USD per million tokens, plusrequest(per-request pricing) andimage(per image) where a provider offers them. Providers above the limit are skipped.quantizations: restrict to providers serving the model at given quantization levels (e.g."fp8","int4").dataCollection:"deny"to use only providers that do not collect user data.zdr:trueto restrict routing to zero-data-retention endpoints.
Model variants
variant appends OpenRouter's :variant suffix to the model id. It is adapter metadata only and is never sent in the request body:
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterText("deepseek/deepseek-v4-pro"),
messages: [{ role: "user", content: "Hello!" }],
modelOptions: {
variant: "free", // requests deepseek/deepseek-v4-pro:free
},
});Accepted values are "free", "nitro", "online", "exacto", "extended", and "thinking". Which of them a model actually offers is up to OpenRouter's catalog.
Plugins
plugins enables OpenRouter's request-level plugins. Each entry is identified by id, with plugin-specific options alongside:
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterText("openai/gpt-6-astra"),
messages: [{ role: "user", content: "Hello!" }],
modelOptions: {
plugins: [{ id: "web", maxResults: 5 }],
},
});Plugin ids include web, file-parser, response-healing, moderation, and auto-router; see the OpenRouter documentation for each plugin's options. Web fetching is not a plugin but a server tool (openrouter:web_fetch); for web search and fetch as tools the model can call, see Provider Tools below.
Reasoning
Set reasoning on chat(). OpenRouter gets it as reasoning.effort:
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterText("anthropic/claude-opus-5.5"),
messages: [{ role: "user", content: "Hello!" }],
reasoning: "high",
});offsendseffort: "none".openRouterTexttakes nobudgetTokens.openRouterResponsesTextsendsbudgetTokensasreasoning.maxTokens, and addssummary: "auto"so the thinking text streams.- The types take the levels of each model. Your editor shows them when you type
reasoning.
Session and metadata
user (a unique user identifier), sessionId, and metadata (up to 16 key-value pairs) are forwarded unchanged. sessionId doubles as OpenRouter's sticky routing key: requests that share it are routed to the same provider to maximize prompt-cache hits, and grouped together for observability.
Prompt caching
chat() sets sessionId to the cache key by default, which is the threadId that you pass. So one conversation stays on one provider and reads its cache. anthropic/* models also get cache markers on the system message, the last tool, and the last message. promptCache: 'long' asks for the 1-hour cache, and promptCache: 'none' sends nothing. See Prompt Caching.
Your own modelOptions.sessionId wins. A cache marker of your own turns the automatic markers off for that request.
Model Options
OpenRouter supports various provider-specific options. Sampling parameters live here too — temperature, topP, and maxCompletionTokens (OpenRouter's token-limit key for the chat adapter) — rather than as root-level props on chat():
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openRouterText("anthropic/claude-sonnet-4.5"),
messages,
modelOptions: {
temperature: 0.7,
topP: 0.9,
maxCompletionTokens: 1024,
},
});
return toServerSentEventsResponse(stream);
}If you previously passed
temperature/topP/maxTokensat the root ofchat(), see Moving Sampling Options into modelOptions.
Chat Completions vs Responses (beta)
OpenRouter exposes two OpenAI-compatible wire formats, and the adapter package ships one of each:
| Adapter | Endpoint | Status | When to use |
|---|---|---|---|
openRouterText | /v1/chat/completions | Stable | Default for almost everything. Broadest model + tool support. |
openRouterResponsesText | /v1/responses | Beta | OpenAI Responses-shaped request/response; richer multi-turn state on OpenAI-style models. |
Both adapters route to any underlying model OpenRouter supports
(anthropic/..., google/..., meta-llama/..., etc.) — the wire format
describes how your client talks to OpenRouter, not which provider answers.
/v1/responses is OpenAI's newer API surface; OpenRouter implements it so
clients that prefer that wire format can use it across the same 300+
model catalogue.
import { chat } from "@tanstack/ai";
import { openRouterResponsesText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterResponsesText("anthropic/claude-sonnet-4.5"),
messages: [{ role: "user", content: "Hello!" }],
});Caveats while the Responses adapter is in beta:
- Function tools are supported; OpenRouter's branded server-tools (web
search, file search, …) are not yet wired through this path — use
openRouterTextif you need those. - If in doubt, prefer
openRouterText. The Chat Completions endpoint has broader provider coverage and feature parity today.
Cost Tracking
OpenRouter reports the actual cost of each request inline on the streamed
response. When present, the adapter forwards it on the terminal RUN_FINISHED
event under usage.cost, with OpenRouter's per-request breakdown under
usage.costDetails. This is the cost OpenRouter itself reports for the
request — it is not computed locally from token counts, so it already
accounts for routing, fallback providers, BYOK, and cached-token pricing. See
OpenRouter's Usage Accounting
docs for the meaning and units of these fields.
import { chat, type RunFinishedEvent, type StreamChunk } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
function isRunFinished(chunk: StreamChunk): chunk is RunFinishedEvent {
return "finishReason" in chunk;
}
for await (const chunk of chat({
adapter: openRouterText("openai/gpt-5"),
messages: [{ role: "user", content: "Hello!" }],
})) {
if (isRunFinished(chunk)) {
console.log("cost:", chunk.usage?.cost);
console.log("breakdown:", chunk.usage?.costDetails);
}
}The same usage (including cost / costDetails) is passed to middleware via
the onUsage and onFinish hooks. When OpenRouter does not report a cost, the
fields are simply absent and the stream completes normally. Both
openRouterText and openRouterResponsesText populate cost when OpenRouter
returns it.
The openRouterText adapter's structuredOutputStream() method includes
received usage on its terminal RUN_ERROR event. This covers parse failures,
truncated responses, empty responses, and SDK errors. When chat() uses this
path, middleware onChunk receives the error event even if the promise rejects.
The onUsage hook reports successful calls. Each adapter call reports usage on
one terminal event.
A reported completionTokensDetails.reasoningTokens value of 0 stays zero.
A missing count stays absent. Errors before any usage arrives have no usage
field. If the consumer stops reading the stream, it cannot receive later events.
Reranking
OpenRouter exposes rerank models through its unified /v1/rerank endpoint
(served via the @openrouter/sdk SDK). Any rerank model OpenRouter offers works
by passing its slug — for example cohere/rerank-v3.5, cohere/rerank-4-fast,
cohere/rerank-4-pro, or nvidia/llama-nemotron-rerank-vl-1b-v2. Use
openRouterRerank with the rerank() activity to reorder candidate documents
by relevance to a query:
import { rerank } from "@tanstack/ai";
import { openRouterRerank } from "@tanstack/ai-openrouter";
const { rerankedDocuments } = await rerank({
adapter: openRouterRerank("cohere/rerank-v3.5"),
query: "talk about rain",
documents: ["sunny day at the beach", "rainy afternoon in the city"],
topN: 2,
});
console.log(rerankedDocuments[0]); // 'rainy afternoon in the city'openRouterRerank reads OPENROUTER_API_KEY from the environment; pass a key
explicitly with createOpenRouterRerank("cohere/rerank-v3.5", "sk-or-..."). The
optional httpReferer / appTitle config fields are forwarded as OpenRouter
attribution headers, just like the chat adapter.
See the Reranking guide for object documents, RAG pipelines, options, and the result shape.
Evaluate
OpenRouter exposes TypeSafe Jev through POST /api/alpha/decisions.
Use openRouterDecider with decide() to ask typed questions about a
shared state:
import { decide, choice, score, boolean } from "@tanstack/ai";
import { openRouterDecider } from "@tanstack/ai-openrouter";
const ticket = {
subject: "Charged twice for the same invoice",
body: "Please refund the extra payment.",
};
const result = await decide({
adapter: openRouterDecider("~typesafe/jev-latest"),
state: ticket,
questions: {
queue: choice({
instructions: "Which team should handle this ticket?",
options: {
billing: "Payments, invoices, refunds",
tech: "Bugs, outages, integrations",
sales: "Pricing, upgrades, new accounts",
},
}),
urgency: score({
instructions: "How urgent is this ticket?",
levels: ["low", "medium", "high"],
}),
refund: boolean({
instructions: "Is the customer asking for a refund?",
}),
},
});
console.log(result.queue.value);
console.log(result.queue.probability);
console.log(result.queue.confidence);
console.log(result.meta.usage);To bill per request, read result.meta.usage.cost. To look the request up later with GET /api/v1/generation?id=, read result.meta.id. result.meta.provider names the upstream provider that served the request.
openRouterDecider reads OPENROUTER_API_KEY from the environment. Pass a
key explicitly with createOpenRouterDecider("~typesafe/jev-latest", "sk-or-...").
Known slugs:
~typesafe/jev-latesttypesafe/jev-1.13typesafe/jev-1.13.0
See the Evaluate guide for question helpers, the result shape, abort, and middleware.
Image Generation
For a React + Start walkthrough with useGenerateImage, open Generate Image.
openRouterImage routes image generation through OpenRouter's
chat-completions surface (modalities: ['image']). Multimodal prompts are
supported — text and image parts are forwarded in order for
image-conditioned generation:
import { generateImage } from "@tanstack/ai";
import { openRouterImage } from "@tanstack/ai-openrouter";
const result = await generateImage({
adapter: openRouterImage("google/gemini-2.5-flash-image"),
prompt: "A watercolor lighthouse at dusk",
size: "1344x768", // mapped to image_config.aspect_ratio ('16:9')
modelOptions: {
image_size: "2K", // resolution (Gemini models)
strength: 0.35, // image-to-image influence, i2i-capable models only
},
});Notes:
- The pathway returns exactly one image per request —
numberOfImages > 1throws instead of silently under-delivering. Make multiple requests if you need multiple candidates. sizemust be one of the ten supportedWIDTHxHEIGHTvalues (it is converted toimage_config.aspect_ratio); anything else throws with the supported list.
Video Generation (Experimental)
openRouterVideo targets OpenRouter's dedicated async video API
(POST /api/v1/videos) — Seedance, Veo 3.1, Wan, Kling, and Sora 2 Pro
through your one OpenRouter key. It follows the jobs/polling architecture
shared by all TanStack AI video adapters:
// Server: create the job, then poll
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openRouterVideo } from "@tanstack/ai-openrouter";
import { withGenerationPersistence } from "@tanstack/ai-persistence";
import { persistence } from "./persistence";
const adapter = openRouterVideo("bytedance/seedance-2.0");
// OpenRouter returns the finished video as a stream, not a URL. Persistence
// stores the stream and sets `url`. Pass the same options to both calls.
const hosting = {
threadId: "videos",
middleware: [
withGenerationPersistence(persistence, {
artifactUrl: (ref) => `/api/artifacts/${ref.artifactId}`,
}),
],
};
const { jobId } = await generateVideo({
adapter,
...hosting,
prompt: [
{ type: "text", content: "Animate this product shot, slow push-in" },
{
type: "image",
source: { type: "url", value: "https://your-cdn.com/product.png" },
metadata: { role: "start_frame" },
},
],
size: "1280x720",
// `duration` is typed per model from the published metadata; coerce raw
// seconds with adapter.snapDuration() or enumerate via adapter.availableDurations().
duration: 8,
});
let status = await getVideoJobStatus({ adapter, jobId, ...hosting });
while (status.status !== "completed" && status.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
status = await getVideoJobStatus({ adapter, jobId, ...hosting });
}
// status.url is your artifact route. status.usage?.cost is the real billed
// cost that the gateway reports.
// Client: track the job with the useGenerateVideo hook
import { useGenerateVideo, fetchServerSentEvents } from "@tanstack/ai-react";
const { generate, result, videoStatus, isLoading } = useGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
});
// result?.url renders directly: <video src={result.url} controls />Sizes, durations, and per-model options (resolution, aspectRatio,
generateAudio, seed, …) are typed and validated per model from
OpenRouter's video model metadata. See
Video Generation for the full lifecycle,
streaming mode, and the image-to-video role-mapping table.
Next Steps
- Getting Started - Learn the basics
- Tools Guide - Learn about tools
- Reranking - Reorder documents by relevance
- Evaluate - Ask typed questions about shared state
Provider Tools
Migrated from
createWebSearchTool? This factory was renamed towebSearchTooland moved to the/toolssubpath in this release. See Migration Guide §6 for the exact before/after.
OpenRouter's gateway exposes web search via a plugin that works across
any proxied chat model. Import it from @tanstack/ai-openrouter/tools.
For the full concept, a comparison matrix, and type-gating details, see Provider Tools.
webSearchTool
Adds web search capability to any OpenRouter-proxied chat model. The factory
accepts OpenRouter's WebSearchConfig directly — pick the engine
(auto, native, exa, firecrawl, or parallel), cap results with
maxResults / maxTotalResults, restrict which sites can appear in results
with allowedDomains / excludedDomains, and optionally pass
searchContextSize or userLocation for finer control.
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
import { webSearchTool } from "@tanstack/ai-openrouter/tools";
const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages: [{ role: "user", content: "What's new in AI this week?" }],
tools: [
webSearchTool({
engine: "exa",
maxResults: 5,
allowedDomains: ["arxiv.org", "openai.com"],
}),
],
});
Supported models: all OpenRouter chat models. See Provider Tools.
webFetchTool
Lets any OpenRouter-proxied chat model fetch the full contents of a URL the
model chooses, instead of running a search. The factory accepts OpenRouter's
WebFetchServerToolConfig directly — pick the fetch engine (auto — the
default, native, openrouter, exa, or firecrawl), cap how much page
content the model receives with maxContentTokens, cap how many fetches the
model can make per request with maxUses, and restrict which URLs the model
can fetch with allowedDomains / blockedDomains.
The
nativeengine routes to the underlying provider's own fetch (for example, Anthropic'sweb_fetchon Claude models). Native fetch capabilities vary, soallowedDomainsandblockedDomainsmay be ignored. Useopenrouter,exa, orfirecrawlif you need consistent behaviour across models.
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
import { webFetchTool } from "@tanstack/ai-openrouter/tools";
const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages: [
{ role: "user", content: "Summarize https://example.com/article" },
],
tools: [
webFetchTool({
engine: "openrouter",
maxContentTokens: 4000,
allowedDomains: ["example.com"],
}),
],
});
Supported models: all OpenRouter chat models. See Provider Tools.
Sign in with OpenRouter (BYOK)
OpenRouter can mint a user-owned API key via OAuth PKCE. Import the helpers from @tanstack/ai-openrouter/pkce. The key is stored under openrouterByok.id (openrouter) and sent as x-byok-openrouter.
Client: start login, then on return save the key into defineByok.
import { useEffect } from "react";
import { defineByok, defaultByokStorage } from "@tanstack/ai-client/byok";
import {
completeOpenRouterPkceIntoByok,
startOpenRouterPkceLogin,
} from "@tanstack/ai-openrouter/pkce";
const byok = defineByok({
storage: defaultByokStorage(),
});
export function OpenRouterSignIn() {
useEffect(() => {
void completeOpenRouterPkceIntoByok(byok);
}, []);
return (
<button
type="button"
onClick={() => {
void startOpenRouterPkceLogin();
}}
>
Sign in with OpenRouter
</button>
);
}
Server: read the same slug the PKCE helper wrote.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { byokMissing, getByokKey } from "@tanstack/ai/byok/server";
import { createOpenRouterText } from "@tanstack/ai-openrouter";
import { openrouterByok } from "@tanstack/ai-openrouter/byok";
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
const apiKey = getByokKey(request, openrouterByok);
if (!apiKey) return byokMissing(openrouterByok);
const stream = chat({
adapter: createOpenRouterText("openai/gpt-5.5", apiKey),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
});
return toServerSentEventsResponse(stream);
}
See Bring Your Own Key for the keyring and passkey storage.
Sign in from a CLI
A CLI or a desktop app has no web page for the redirect, so the user would have to copy a key by hand. openrouterSignIn() signs the user in with the browser and gives your app the key:
import { chat } from "@tanstack/ai";
import { createOpenRouterText } from "@tanstack/ai-openrouter";
import { openrouterSignIn } from "@tanstack/ai-openrouter/pkce";
const signIn = openrouterSignIn();
const apiKey = await signIn({
open: (url) => console.log(`Open this URL to sign in: ${url}`),
});
const stream = chat({
adapter: createOpenRouterText("openai/gpt-6.1-sol", apiKey),
messages: [{ role: "user", content: "Hello!" }],
});
signInstarts a one-time listener on127.0.0.1, on a random port.- It calls
openwith the OpenRouter sign-in URL. Open that URL in the browser. - The user signs in and approves. The browser shows
You are signed in to OpenRouter. You can close this tab. signInresolves with the API key, and the listener closes.
The sign-in uses PKCE and a random state. It stops after 10 minutes. To change the time, pass openrouterSignIn({ timeoutMs }). To cancel it, pass signal to signIn.
The listener runs on the computer of your app. So the sign-in works only when your app and the browser are on the same computer. In a hosted app, use the browser flow above.