A lightweight Node.js proxy server and browser-based chat UI that connects to the OpenRouter API. OpenRouter provides a unified interface to hundreds of language models (OpenAI, Anthropic, Google, Meta, Mistral, and more) through a single OpenAI-compatible endpoint.
- Model selector — choose from every model available on your OpenRouter account; the list is loaded dynamically on startup
- Streaming responses — assistant replies stream token-by-token via Server-Sent Events
- Markdown rendering — assistant messages are rendered as formatted HTML (headings, lists, code blocks, tables, blockquotes) via marked with DOMPurify sanitization
- System prompt — optional, collapsible system prompt applied to every request in the session
- Multi-turn conversation — full message history is maintained in the browser and sent on each request
- Image generation — separate page for generating, describing, and improving prompts for images via OpenRouter image models
- Error handling — maps API error codes (invalid key, no credits, rate limits, etc.) to plain-language messages
- Key via URL — no server-side secrets; each user supplies their own API key in the URL
- Privacy notice — a data-protection modal is shown on every page load of the chat and image tools, informing users that nothing is persisted beyond the browser session
- Node.js 18 or later (built-in
fetchis required) - An OpenRouter API key
# 1. Install dependencies
npm install
# 2. Start the server (with live reload)
npm run dev
# 3. Open the app — replace YOUR_KEY with your OpenRouter key
open http://localhost:1515/chat.html?key=YOUR_KEYFor production:
npm startThe port defaults to 3000 and can be overridden with a PORT environment variable or in .env:
PORT=1515The main chat application. Requires a key URL parameter containing your OpenRouter API key.
http://localhost:1515/chat.html?key=YOUR_KEY
If no key is present the page replaces itself with an instruction screen. Assistant responses are rendered as formatted markdown (headings, lists, fenced code blocks with syntax-aware theming, tables, blockquotes).
Three image-related tools in one page: Generate Image, Image to Prompt, and Improve Prompt. Requires the same key URL parameter and links back to the chat page preserving the key.
http://localhost:1515/image.html?key=YOUR_KEY
A standalone form that accepts an API key. After submitting, the user chooses where to go: Images opens /image.html and Chat opens /chat.html, both with ?key= appended. Not linked from the main application — share this URL with users who need a guided entry point.
http://localhost:1515/key.html
Shows details for the key passed in the URL, fetched from GET https://openrouter.ai/api/v1/key. Displays label, credit limit, remaining credits, limit reset cadence, all-time/daily/weekly/monthly usage (regular and BYOK), free-tier status, and whether BYOK usage counts toward the limit.
http://localhost:1515/credits.html?key=YOUR_KEY
magnific-forwarding/
├── server.js # Express proxy server
├── package.json
├── .env # PORT override (optional)
├── .env.example
└── public/
├── chat.html # Chat UI
├── chat.js # Chat client JS
├── image.html # Image tools UI (generate / image-to-prompt / improve)
├── key.html # Standalone API key entry page
├── credits.html # API key details & usage page
├── app.js # Image tools client JS
├── privacy-modal.js # Privacy/data-protection modal (shown on every session start)
└── style.css # Shared styles
The key is read from ?key= in the browser URL. The frontend attaches it as an x-api-key header on every request to the local server. The server converts it to Authorization: Bearer <key> before forwarding to OpenRouter. The key is never written to disk or logged.
Because multiple users each supply their own key, a single running server instance can serve many users simultaneously.
All routes require the x-api-key request header.
| Method | Route | Description |
|---|---|---|
GET |
/api/models |
Returns the list of models available to the key, proxied from GET https://openrouter.ai/api/v1/models |
POST |
/api/chat |
Streams a chat completion, proxied from POST https://openrouter.ai/api/v1/chat/completions |
{
"model": "openai/gpt-4o",
"messages": [
{ "role": "user", "content": "Hello" }
],
"system": "You are a helpful assistant."
}system is optional. When provided it is prepended as a system role message before messages. The server always requests "stream": true from OpenRouter and forwards the SSE response directly to the browser.
The server passes through OpenRouter's SSE stream unchanged. Each event is a standard OpenAI-compatible chunk:
data: {"choices":[{"delta":{"content":"Hello"},"index":0}]}
data: [DONE]
Errors that arrive inside the stream (e.g. mid-generation quota exhaustion) include an error field and are surfaced to the user by the frontend.
HTTP error codes from OpenRouter are forwarded to the browser. The frontend maps them to user-readable messages:
| Status | Message shown |
|---|---|
| 401 | Invalid or missing API key |
| 402 | Insufficient credits — add credits at openrouter.ai |
| 403 | Access denied |
| 400 / 422 | Bad request — surfaces the API's own message |
| 429 | Rate limit reached, retry shortly |
| 500 | OpenRouter internal error |
| 502 / 503 / 504 | Service temporarily unavailable |
On startup the frontend fetches /api/models and populates the model selector, grouping models by provider. While the request is in flight a curated fallback list of seven popular models is shown so the user can start chatting immediately. If the fetch fails, the fallback list remains.
See LICENSE.