Skip to content
Adapters

OpenRouter Adapter

openRouterText calls 300+ models from OpenAI, Anthropic, Google, Meta, Mistral, and others with one API key.

1. Install

shell
npm i @tanstack/ai-openrouter

2. Set the API key

Put the key in the environment of your server:

shell
OPENROUTER_API_KEY=sk-or-...

openRouterText and the other short factories read OPENROUTER_API_KEY. To pass the key in code, use the create* factory of the same adapter:

ts
import { createOpenRouterText } from "@tanstack/ai-openrouter";

const adapter = createOpenRouterText("openai/gpt-6.1-sol", process.env.OPENROUTER_API_KEY!);

3. Stream a chat from the server

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: openRouterText("openai/gpt-6.1-sol"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}

To use a different model, change the id. Ids have the shape provider/model-name, for example anthropic/claude-sonnet-5.5 or google/gemini-3.1-pro-preview.

4. Render the chat

The client is the same for every provider. Point useChat at your route:

tsx
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        sendMessage(input);
        setInput("");
      }}
    >
      {messages.map((message) => (
        <p key={message.id}>
          {message.parts
            .map((part) => (part.type === "text" ? part.content : ""))
            .join("")}
        </p>
      ))}
      <input value={input} onChange={(event) => setInput(event.target.value)} />
    </form>
  );
}

For Vue, Solid, Svelte, Angular, and the other frameworks, see Quick Start.

You now have a streaming chat on OpenRouter. Tools and Structured Outputs work the same as with every adapter.

Model routing

A model can be down, or slow on one provider. Give OpenRouter a fallback list in models. When the primary model is not available, OpenRouter tries the models in order:

ts
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";

const stream = chat({
  adapter: openRouterText("openrouter/auto"),
  messages: [{ role: "user", content: "Hello!" }],
  threadId: "thread-1",
  modelOptions: {
    models: [
      "openai/gpt-6.1-sol",
      "anthropic/claude-sonnet-5.5",
      "google/gemini-3.1-pro-preview",
    ],
  },
});

To pick which upstream providers serve a model, set modelOptions.provider:

ts
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!" }],
  threadId: "thread-1",
  modelOptions: {
    provider: {
      order: ["groq", "together"], // provider slugs, in priority order
      allowFallbacks: false, // fail instead of routing elsewhere
    },
  },
});

For all the provider fields, model variants such as :free, and plugins, see OpenRouter: Advanced.

Provider tools

OpenRouter runs web search and web fetch for any chat model. Import the tools from @tanstack/ai-openrouter/tools:

ts
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-6.1-sol"),
  messages: [{ role: "user", content: "What's new in AI this week?" }],
  threadId: "thread-1",
  tools: [
    webSearchTool({
      engine: "exa",
      maxResults: 5,
      allowedDomains: ["arxiv.org", "openai.com"],
    }),
  ],
});
  • webSearchTool: search the web.
  • webFetchTool: read the full page at a URL that the model picks.

Both work on all OpenRouter chat models. For every option of each tool, see OpenRouter: Advanced. For how provider tools compare across adapters, see Provider Tools.

Tools and structured output together

You can pass tools and outputSchema on one chat() call. On some models, OpenRouter returns the typed object in the same streaming request. Then no second call is necessary.

This happens only when every model that can get the request is in OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS. To check a model before you send:

ts
import { OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS } from "@tanstack/ai-openrouter/model-meta";

console.log(OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS.has("openai/gpt-6.1-sol")); // true

For the full server and client code and the rules of the set, see OpenRouter: Advanced.

Images

ts
import { generateImage } from "@tanstack/ai";
import { openRouterImage } from "@tanstack/ai-openrouter";

const result = await generateImage({
  adapter: openRouterImage("google/gemini-nano-banana-2.1"),
  prompt: "A watercolor lighthouse at dusk",
  threadId: "thread-1",
  size: "1344x768", // sent as image_config.aspect_ratio ("16:9")
  modelOptions: {
    image_size: "2K", // resolution (Gemini models)
  },
});

console.log(result.images);
  • You get one image for each request. numberOfImages more than 1 throws an error.
  • size takes one of ten WIDTHxHEIGHT values. Other values throw an error that lists the valid sizes.
  • The prompt can mix text and image parts, for image-to-image work. modelOptions.strength sets how much the input image controls the result, on models that support it.

For a React and TanStack Start walkthrough with useGenerateImage, see Generate Image. See also Image Generation.

Video

openRouterVideo makes videos with Seedance, Veo 3.1, Wan, Kling, and more, through the async video API of OpenRouter (POST /api/v1/videos). Video generation is experimental. For the full server and client code, see OpenRouter: Advanced.

Reranking

openRouterRerank puts documents in order of how well they match a query:

ts
import { rerank } from "@tanstack/ai";
import { openRouterRerank } from "@tanstack/ai-openrouter";

const { rerankedDocuments } = await rerank({
  adapter: openRouterRerank("cohere/rerank-4-pro"),
  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"

Any rerank model on OpenRouter works. Known ids: cohere/rerank-v3.5, cohere/rerank-4-fast, cohere/rerank-4-pro, nvidia/llama-nemotron-rerank-vl-1b-v2. See the Reranking guide.

Evaluate

Sometimes you need an answer that your code can branch on, not chat text. Use openRouterDecider with decide() to ask typed questions about one shared state. The adapter calls TypeSafe Jev through POST /api/alpha/decisions:

ts
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);
  • result.meta.usage.cost: the cost of the request, to bill per request.
  • result.meta.id: the id to look the request up later with GET /api/v1/generation?id=.
  • result.meta.provider: the upstream provider that served the request.

Known ids: ~typesafe/jev-latest, typesafe/jev-1.13, typesafe/jev-1.13.0. See the Evaluate guide.

Summarization

ts
import { summarize } from "@tanstack/ai";
import { openRouterSummarize } from "@tanstack/ai-openrouter";

const result = await summarize({
  adapter: openRouterSummarize("google/gemini-3.8-flash"),
  text: "Your long text to summarize...",
  threadId: "thread-1",
  maxLength: 100,
  style: "concise", // "concise" | "bullet-points" | "paragraph"
});

console.log(result.summary);

Cost tracking

OpenRouter reports the real cost of each request. The adapter puts it on the last RUN_FINISHED event as usage.cost, with the breakdown in usage.costDetails. This cost includes routing, fallback providers, BYOK, and cached-token prices. See Cost tracking for a sample.

Sign in with OpenRouter (BYOK)

Your users can sign in with their own OpenRouter account. Then the requests use their key and their credit. The helpers are in @tanstack/ai-openrouter/pkce. For the browser and the CLI flows, see OpenRouter: Advanced.

Models and options

Model options

Provider options go in modelOptions. This includes sampling options such as temperature and maxCompletionTokens (the token limit of the chat adapter). The type only allows the options that the selected model supports. For example, Claude Sonnet 5.5 does not take topP, so topP is a type error for that model:

ts
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";

const stream = chat({
  adapter: openRouterText("anthropic/claude-sonnet-5.5"),
  messages: [{ role: "user", content: "Hello!" }],
  threadId: "thread-1",
  modelOptions: {
    temperature: 0.7,
    maxCompletionTokens: 1024,
  },
});

The option names are the camelCase names of the OpenRouter SDK. Sampling options do not go at the root of chat(). See Moving Sampling Options into modelOptions.

Reasoning

Set reasoning on chat(). OpenRouter gets it as reasoning.effort:

ts
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!" }],
  threadId: "thread-1",
  reasoning: "high",
});
  • off sends effort: "none".
  • openRouterText takes no budgetTokens. openRouterResponsesText sends budgetTokens as reasoning.maxTokens, and adds summary: "auto" so that the thinking text streams.
  • The types know the levels of each model. Your editor shows them when you type reasoning.

See Reasoning.

Models

Model ids have the shape provider/model-name:

plaintext
openai/gpt-6.1-sol
anthropic/claude-sonnet-5.5
google/gemini-3.1-pro-preview
meta-llama/llama-4-maverick
deepseek/deepseek-v4-pro

See the full list at openrouter.ai/models. The package exports the known ids:

  • @tanstack/ai-openrouter/model-meta: OPENROUTER_CHAT_MODELS, OPENROUTER_IMAGE_MODELS, OPENROUTER_VIDEO_MODELS.
  • @tanstack/ai-openrouter: OPENROUTER_RERANK_MODELS, OPENROUTER_EVALUATE_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.

Advanced

These topics are on OpenRouter: Advanced:

  • Configuration: base URL, app attribution headers, and wrapFetch.
  • Retries for rate limits, and for an error in the stream.
  • The Responses adapter (openRouterResponsesText, beta).
  • Provider preferences, model variants, plugins, and session metadata.
  • Prompt caching and JSON mode.
  • Tools and structured output in one call, with the full code.
  • webFetchTool, cost tracking, video generation, and Sign in with OpenRouter.
  • The API reference.