Send your first message and stream a response with the OpenRouter SDK
Goal: Learn the fundamentals of OpenRouter by building a TypeScript chat app that sends messages and streams responses through OpenRouter.Outcome: A working multi-turn conversation loop that can talk to any of the 600+ models available on the platform by changing a single string.
Want to get started faster? Copy this prompt into your coding agent.
Set up a new Node.js project and add the OpenRouter client SDK. The SDK is ESM-only, so set the package type to module. Install tsx so you can run the TypeScript examples directly.
mkdir openrouter-chat && cd openrouter-chatnpm init -ynpm pkg set type=modulenpm install @openrouter/sdknpm install --save-dev tsx
Create chat.ts with a client instance and a single Responses API request. The apiKey reads from the environment so you never hard-code credentials.
import { OpenRouter } from '@openrouter/sdk';const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY,});const response = await client.responses.send({ responsesRequest: { model: 'google/gemini-3.1-flash-lite', input: 'Say hello in one sentence.', },});if (!('output' in response)) { throw new Error('Expected a non-streaming response');}const message = [...response.output].reverse().find((item) => item.type === 'message');const text = message?.content.find((part) => part.type === 'output_text');console.log(text?.text);console.log({ inputTokens: response.usage?.inputTokens, outputTokens: response.usage?.outputTokens,});
Run it with your API key:
OPENROUTER_API_KEY=sk-or-v1-... npx tsx chat.ts
You should see a single text response printed to the console. The text is in the output_text part of the message item in response.output, and the SDK returns token usage in camelCase fields such as inputTokens and outputTokens. The response.output array follows the same shape as the Responses API response. Without stream: true, client.responses.send is typed to return the full response, so the 'output' in response check is only a runtime safety guard.
Streaming returns text as it is generated instead of waiting for the full response. Pass stream: true and iterate over the returned event stream. Each response.output_text.delta event contains a delta with the new text fragment.
import { OpenRouter } from '@openrouter/sdk';const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY,});const stream = await client.responses.send({ responsesRequest: { model: 'google/gemini-3.1-flash-lite', input: 'Explain how routers work in three sentences.', stream: true, },});if ('output' in stream) { throw new Error('Expected a streaming response');}for await (const event of stream) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); }}console.log();
Text now prints incrementally. See the Streaming reference for the full SSE event format.
Multi-turn works by sending the full conversation history with each request. The model uses all previous turns as context. Append each user input and assistant response to an input array of { role, content } items before the next call.
import { OpenRouter } from '@openrouter/sdk';import * as readline from 'node:readline';const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY,});const input: { role: 'user' | 'assistant'; content: string }[] = [];const rl = readline.createInterface({ input: process.stdin, output: process.stdout,});function ask(): void { rl.question('You: ', async (line) => { if (line.toLowerCase() === 'exit') { rl.close(); return; } input.push({ role: 'user', content: line }); const stream = await client.responses.send({ responsesRequest: { model: 'google/gemini-3.1-flash-lite', input, stream: true, }, }); if ('output' in stream) { throw new Error('Expected a streaming response'); } let response = ''; let completed = false; process.stdout.write('Assistant: '); for await (const event of stream) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); response += event.delta; } if (event.type === 'response.completed') { completed = true; } } console.log(); // A stream that ends with response.failed or response.incomplete leaves a // partial answer; keep only completed turns in the history. if (completed) { input.push({ role: 'assistant', content: response }); } else { input.pop(); console.log('(The response did not complete, so that turn was not saved.)'); } ask(); });}ask();
Run the file and type messages. The model remembers prior turns because the full input array is sent with each request. Type exit to quit.
OpenRouter gives you access to hundreds of models through one API. Change the model string to switch providers, no other code changes needed.
// Use OpenAI's latest chat modelmodel: 'openai/gpt-chat-latest',// Use Anthropic Claude Sonnet latestmodel: '~anthropic/claude-sonnet-latest',// Use a free modelmodel: 'openrouter/free',