OpenRouter 어댑터
OpenRouter는 TanStack AI의 첫 공식 AI 파트너이며 대부분의 프로젝트에 권장되는 시작점입니다. 단일 API 키와 통합 인터페이스를 통해 OpenAI, Anthropic, Google, Meta, Mistral 등의 300개 이상 모델에 액세스할 수 있습니다.
설치
npm install @tanstack/ai-openrouter
기본 사용법
import { chat } from "@tanstack/ai";
import { openRouterText } from "@tanstack/ai-openrouter";
const stream = chat({
adapter: openRouterText("openai/gpt-5"),
messages: [{ role: "user", content: "Hello!" }],
});
구성
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
},
);
사용 가능한 모델
OpenRouter는 다양한 제공업체의 300개 이상 모델에 액세스할 수 있도록 합니다. 모델은 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"
전체 목록은 openrouter.ai/models에서 확인합니다.
예제: 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);
}
예제: 도구 사용
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);
}
도구와 구조화된 출력 함께 사용
하나의 chat() 호출에 tools와 outputSchema를 모두 전달할 수 있습니다. 일부 업스트림 모델에서는 OpenRouter가 동일한 스트리밍 요청에서 타입이 지정된 객체를 반환할 수 있으므로 엔진이 두 번째 최종화 호출을 수행하지 않습니다.
이는 요청을 받을 수 있는 모든 모델이 OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS 에 있을 때만 발생합니다. 이 집합은 OpenRouter 의 카탈로그에서 매번 모델 동기화 시 생성되며, 모든 채팅 모델 중 supported_parameters 에 structured_outputs, tools 및 tool_choice 를 포함합니다 (Claude 4.5+, Gemini 2.5+, GPT-4o+, Grok 4, DeepSeek V3+, Llama 3.1+ 등). OpenRouter 가 플래그로 표시하지 않는 모델인 anthropic/claude-opus-4.1 는 레거시 두 호출 경로에 남습니다.
modelOptions.models의 폴백 중 하나라도 이 목록에 없으면 OpenRouter는 두 호출 경로를 유지합니다. :nitro와 같은 라우팅 접미사는 이 조건을 변경하지 않습니다.
전송하기 전에 모델을 확인해야 한다면 @tanstack/ai-openrouter/model-meta에서 목록을 가져옵니다.
import { OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS } from "@tanstack/ai-openrouter/model-meta";
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS.has("openai/gpt-5.5");
<!-- validator가 원문의 여러 줄 인라인 코드 오류에서 추출하는 토큰을 숨겨 보존합니다. -->
<!-- openRouterText openRouterResponsesText -->
<!-- useChat({ --> <!-- outputSchema }) still reads partial and final -->
Chat Completions(openRouterText)와 Responses(openRouterResponsesText) 어댑터는 모두 이 경로에서 스키마를 첨부합니다.
클라이언트는 변경되지 않으며 **useChat({ outputSchema })**에서도 partial과 final을 그대로 읽습니다.
서버(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);
}
서버(Responses). Chat Completions 예제와 동일한 tools와 outputSchema를 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);
}
클라이언트:
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,
});
이벤트 순서는 도구를 사용한 구조화된 출력을, 이 경로에서 structuredOutput 단계가 동작하는 방식은 미들웨어를 참조합니다.
브라우저에서 사용해 보려면 examples/ts-react-chat을 실행하고 /generations/openrouter-combined를 엽니다. 이 페이지에는 도구 호출, 타입이 지정된 객체, 어댑터 호출 횟수가 표시됩니다. structuredOutputStream은 0으로 유지되어야 합니다.
환경 변수
환경 변수에 API 키를 설정합니다.
OPENROUTER_API_KEY=sk-or-...
모델 라우팅
OpenRouter는 요청을 가장 적합한 사용 가능한 제공업체로 자동 라우팅할 수 있습니다.
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);
}
모델 옵션
OpenRouter는 다양한 제공업체별 옵션을 지원합니다. 샘플링 매개변수인 temperature, topP, maxCompletionTokens(OpenRouter가 chat 어댑터에 사용하는 토큰 제한 키)도 chat()의 루트 수준 prop이 아니라 여기에 지정합니다.
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);
}
이전에
chat()의 루트에temperature/topP/maxTokens를 전달했다면 샘플링 옵션을 modelOptions로 이동을 참조합니다.
Chat Completions와 Responses 비교(beta)
OpenRouter는 OpenAI와 호환되는 두 가지 와이어 형식을 제공하며, 어댑터 패키지는 각각 하나씩 제공합니다.
| 어댑터 | 엔드포인트 | 상태 | 사용 시기 |
|---|---|---|---|
openRouterText | /v1/chat/completions | 안정 | 거의 모든 기본값. 가장 광범위한 모델 및 도구 지원. |
openRouterResponsesText | /v1/responses | 베타 | OpenAI Responses 형식의 요청/응답; OpenAI 스타일 모델에서 더 풍부한 다중 턴 상태. |
두 어댑터 모두 OpenRouter가 지원하는 모든 기반 모델
(anthropic/..., google/..., meta-llama/... 등)으로 라우팅합니다. 와이어 형식은 클라이언트가 OpenRouter와 통신하는 방식을 설명하며, 어떤 제공업체가 응답하는지를 설명하지 않습니다.
(/v1/responses는 OpenAI의 최신 API 표면입니다.) OpenRouter는 이 형식을 구현하므로 해당 와이어 형식을 선호하는 클라이언트가 동일한 300개 이상의 모델 카탈로그에서 사용할 수 있습니다.
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!" }],
});
Responses 어댑터가 beta인 동안의 주의사항:
- 함수 도구는 지원되지만 OpenRouter의 브랜드 서버 도구(웹 검색, 파일 검색 등)는 아직 이 경로에 연결되지 않았습니다. 이러한 도구가 필요하면
openRouterText를 사용합니다. - 확실하지 않다면
openRouterText를 우선 사용합니다. 현재 Chat Completions 엔드포인트가 더 폭넓은 제공업체와 기능 동등성을 지원합니다.
비용 추적
OpenRouter는 스트리밍 응답에 각 요청의 실제 비용을 인라인으로 보고합니다. 비용이 있으면 어댑터는 종료 RUN_FINISHED 이벤트의 usage.cost에 비용을 전달하고, 요청별 세부 내역은 usage.costDetails에 전달합니다. 이는 OpenRouter 자체가 요청에 대해 보고하는 비용이며 토큰 수를 기준으로 로컬에서 계산한 값이 아닙니다. 따라서 라우팅, 폴백 제공업체, BYOK, 캐시된 토큰 가격이 이미 반영되어 있습니다. 이러한 필드의 의미와 단위는 OpenRouter의 사용량 계산 문서를 참조합니다.
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);
}
}
동일한 usage(cost / costDetails 포함)는 onUsage 및 onFinish 훅을 통해 미들웨어에 전달됩니다. OpenRouter가 비용을 보고하지 않으면 해당 필드는 없어지고 스트림은 정상적으로 완료됩니다. openRouterText와 openRouterResponsesText 모두 OpenRouter가 비용을 반환할 때 비용을 채웁니다.
재순위 지정
OpenRouter는 통합 /v1/rerank 엔드포인트(@openrouter/sdk SDK를 통해 제공)를 통해 재순위 모델을 노출합니다. cohere/rerank-v3.5, cohere/rerank-4-fast, cohere/rerank-4-pro, nvidia/llama-nemotron-rerank-vl-1b-v2와 같은 slug를 전달하면 OpenRouter가 제공하는 모든 재순위 모델을 사용할 수 있습니다. rerank() 작업에서 openRouterRerank를 사용해 쿼리와의 관련성에 따라 후보 문서의 순서를 변경합니다.
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는 환경에서 OPENROUTER_API_KEY를 읽습니다. createOpenRouterRerank("cohere/rerank-v3.5", "sk-or-...")로 키를 명시적으로 전달할 수도 있습니다. 선택적 httpReferer / appTitle 구성 필드는 chat 어댑터와 마찬가지로 OpenRouter attribution 헤더로 전달됩니다.
객체 문서, RAG 파이프라인, 옵션, 결과 형태는 재순위 지정 가이드를 참조합니다.
이미지 생성
openRouterImage는 OpenRouter의 chat-completions 표면(modalities: ['image'])을 통해 이미지 생성을 라우팅합니다. 멀티모달 프롬프트를 지원하며, 이미지 조건부 생성을 위해 텍스트 및 이미지 파트가 순서대로 전달됩니다.
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
},
});
참고:
- 이 경로는 요청당 정확히 하나의 이미지를 반환합니다.
numberOfImages > 1이면 결과를 조용히 부족하게 반환하지 않고 오류를 발생시킵니다. 여러 후보가 필요하면 여러 번 요청합니다. size는 지원되는 10개의WIDTHxHEIGHT값 중 하나여야 하며(image_config.aspect_ratio로 변환됨), 그 외의 값은 지원 목록과 함께 오류를 발생시킵니다.
비디오 생성(실험적)
openRouterVideo는 OpenRouter 전용 비동기 비디오 API
(POST /api/v1/videos)를 대상으로 하며, 하나의 OpenRouter 키로 Seedance, Veo 3.1, Wan, Kling, Sora 2 Pro를 사용할 수 있습니다. 모든 TanStack AI 비디오 어댑터가 공유하는 작업/폴링 아키텍처를 따릅니다.
// Server: create the job, then poll
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openRouterVideo } from "@tanstack/ai-openrouter";
const adapter = openRouterVideo("bytedance/seedance-2.0");
const { jobId } = await generateVideo({
adapter,
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 });
while (status.status !== "completed" && status.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
status = await getVideoJobStatus({ adapter, jobId });
}
// status.url is a data: URL (OpenRouter download URLs require the API key,
// so the adapter downloads server-side); status.usage?.cost is the real
// billed cost reported by the gateway.
// 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 />
크기, 기간 및 모델별 옵션(resolution, aspectRatio, generateAudio, seed 등)은 OpenRouter의 비디오 모델 메타데이터를 기준으로 모델별 타입 지정 및 검증이 이루어집니다. 전체 수명 주기, 스트리밍 모드, 이미지-비디오 역할 매핑 표는 비디오 생성을 참조합니다.
다음 단계
제공업체 도구
createWebSearchTool에서 마이그레이션했습니까? 이 팩토리는 이번 릴리스에서webSearchTool로 이름이 변경되었으며/tools하위 경로로 이동했습니다. 정확한 변경 전후 내용은 마이그레이션 가이드 §6을 참조합니다.
OpenRouter의 게이트웨이는 프록시된 모든 chat 모델에서 작동하는 플러그인을 통해 웹 검색을 제공합니다. @tanstack/ai-openrouter/tools에서 가져옵니다.
전체 개념, 비교 매트릭스, 타입 게이팅 세부 정보는 제공업체 도구를 참조합니다.
webSearchTool
OpenRouter로 프록시된 모든 chat 모델에 웹 검색 기능을 추가합니다. 이 팩토리는 OpenRouter의 WebSearchConfig를 직접 받습니다. engine을 선택하고
(auto, native, exa, firecrawl, parallel 중 하나), maxResults / maxTotalResults로 결과 수를 제한하고, allowedDomains / excludedDomains로 결과에 표시될 사이트를 제한하며, 필요에 따라 searchContextSize 또는 userLocation을 전달해 세부적으로 제어합니다.
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"],
}),
],
});
지원 모델: 모든 OpenRouter chat 모델입니다. 제공업체 도구를 참조합니다.
webFetchTool
OpenRouter로 프록시된 chat 모델이 검색을 실행하는 대신 모델이 선택한 URL의 전체 콘텐츠를 가져올 수 있도록 합니다. 이 팩토리는 OpenRouter의 WebFetchServerToolConfig를 직접 받습니다. 가져오기 engine(auto(기본값), native, openrouter, exa, firecrawl 중 하나)을 선택하고, maxContentTokens로 모델에 전달할 페이지 콘텐츠의 양을 제한하며, maxUses로 요청당 모델이 수행할 수 있는 가져오기 횟수를 제한하고, allowedDomains / blockedDomains로 모델이 가져올 수 있는 URL을 제한합니다.
native엔진은 기반 제공업체의 자체 가져오기 기능으로 라우팅합니다(예: Claude 모델의 Anthropicweb_fetch). 기본 가져오기 기능은 제공업체마다 다르므로allowedDomains와blockedDomains가 무시될 수 있습니다. 모델 간에 일관된 동작이 필요하면openrouter,exa또는firecrawl을 사용합니다.
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"],
}),
],
});
지원 모델: 모든 OpenRouter chat 모델입니다. 제공업체 도구를 참조합니다.
OpenRouter로 로그인(BYOK)
OpenRouter는 OAuth PKCE를 통해 사용자 소유 API 키를 발급할 수 있습니다. @tanstack/ai-openrouter/pkce에서 헬퍼를 가져옵니다. 키는 openrouterByok.id(openrouter) 아래에 저장되고 x-byok-openrouter로 전송됩니다.
클라이언트: 로그인을 시작한 다음 돌아오면 키를 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>
);
}
서버: PKCE 헬퍼가 작성한 동일한 slug를 읽습니다.
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);
}
키링 및 패스키 저장소는 Bring Your Own Key를 참조합니다.