Local, front-to-back chat experience that talks to OpenRouter as the model gateway. React + TypeScript on the frontend, Express + TypeScript on the backend, and OpenTelemetry traces flowing to a local Jaeger instance.
- Project Introduction
- Technical Architecture
- Installation & Setup
- Running Jaeger
- OpenTelemetry Tracing
- Usage Guide
- Project Structure
- Developer Experience Notes
- Bonus Features
- Conclusion
- Local chat UI that sends messages through OpenRouter to any supported model.
- Features: send and receive messages, view and manage chat history, choose models exposed by the backend, and (optionally) include multimodal uploads when wired in.
- Ships with sensible defaults: in-memory history, a health endpoint, and friendly fallbacks if a model is temporarily unavailable.
- Backend (apps/backend): Node.js + Express + TypeScript. Routes for chat, model discovery, and history. In-memory session store, graceful shutdown, and a health endpoint at
/health. - Frontend (apps/frontend): React + TypeScript (Create React App). Chat UI with history sidebar, inline rename, delete confirmation, and auto-scrolling composer.
- Telemetry: OpenTelemetry NodeSDK with auto-instrumentation (Express + HTTP) plus custom spans for chat processing, OpenRouter calls, history operations, and in-memory storage.
- Tracing backend: Jaeger (local Docker container) receiving OTLP/HTTP on
http://localhost:4318/v1/traceswith UI onhttp://localhost:16686.
- Node.js 18+ and npm 9+.
- Docker (for Jaeger).
- An OpenRouter API key.
git clone https://github.com/<your-org>/OpenRouter-AI-Chat-Platform.git
cd OpenRouter-AI-Chat-PlatformThe repo uses npm workspaces with a shared types package.
npm install # installs backend, frontend, and shared typesIf you prefer installing individually:
cd apps/backend && npm install
cd ../frontend && npm installCreate apps/backend/.env:
OPENROUTER_API_KEY=sk-***your-key***
PORT=3001
APP_URL=http://localhost:3000
JAEGER_ENDPOINT=http://localhost:4318/v1/tracesOptional frontend override (only if your backend is not on http://localhost:3001):
# apps/frontend/.env
REACT_APP_API_URL=http://localhost:3001- Start Jaeger (see the dedicated section below).
- Start the backend:
cd apps/backend
npm run dev
# or production mode:
npm run build
npm startHealth check: curl http://localhost:3001/health.
- Start the frontend:
cd apps/frontend
npm startThe UI runs at http://localhost:3000 and proxies requests to the backend URL.
Bring up Jaeger and the backend together (frontend still runs with npm start):
OPENROUTER_API_KEY=sk-***your-key*** docker compose up -d jaeger backendBackend will listen on http://localhost:3001 and send traces to the Jaeger collector in the compose network.
- Start Jaeger only:
docker compose up -d jaeger- UI:
http://localhost:16686 - OTLP/HTTP collector (what the backend uses):
http://localhost:4318/v1/traces - After sending a few chat requests, open Jaeger UI, search for service
openrouter-chat-backend, and inspect traces by endpoint (/api/chat,/api/history,/api/models).
- What is collected
- Server spans for every HTTP request (method, route, status, user agent, client IP) via
requestTracingMiddleware. - Auto-instrumented spans for Express and outbound HTTP (OpenRouter fetch calls).
- Custom spans for:
openrouter.chatandopenrouter.getModels(API calls, model id, token usage, response id).- Chat flow (
chat.sendMessage) including validation, session stitching, and fallback responses. - History flows (
history.listSessions,history.getSession,history.updateTitle,history.deleteSession,history.clearAll). - In-memory store operations (
memoryStore.*) so cache behavior is traceable.
- Resource metadata includes service name/version and environment for clear attribution in Jaeger.
- Server spans for every HTTP request (method, route, status, user agent, client IP) via
- Send a message: Open
http://localhost:3000, click “New Chat”, type in the composer, press Enter or the send button. Messages stream through the backend to OpenRouter; responses appear in the thread. - Switch models: The backend exposes
/api/models. Update the model passed inapps/frontend/src/hooks/useChatStore.tsx(default isamazon/nova-2-lite-v1:free) or wire a dropdown that callsbackendClient.getModels()and passes the selectedmodeltosendUiMessages. - View history: Sidebar lists sessions with last-updated timestamp. Click to load a session, double-click to rename, use the trash icon (with confirmation) to delete.
- Optional multimodal upload: If you enable file/image uploads, pass OpenRouter’s multimodal message shape to the backend (for example,
contentas an array withtype: "text"andtype: "image_url"items). ExtendApiMessageinpackages/sharedaccordingly, allow the UI composer to attach files, and the backend will forward the enrichedmessagestoroutingService.chatunchanged. - Reset session: Use the “New Chat” button to start a fresh conversation; history persists in-memory until the backend restarts.
apps/
backend/
src/
index.ts # Express entrypoint + health + shutdown
middleware/requestTracingMiddleware.ts
routes/{chat,models,history}.ts
services/{routingService,telemetryService,tracingUtils,openrouter-types}.ts
db/memoryStore.ts
Dockerfile
frontend/
src/
api/backendClient.ts # REST client
components/{ChatPage,ChatWindow,Sidebar,ChatTitle}
hooks/useChatStore.tsx # chat state, history, sending
packages/
shared/ # reused API/UI types
docker-compose.yml # Jaeger + backend
- TypeScript everywhere: Shared types ensure backend responses and frontend usage stay aligned.
- Auto-instrumentation first: Express and HTTP spans are automatic;
runWithSpanandrunWithSpanSyncadd business-level spans where it matters. - Stability and error handling: Backend returns friendly fallbacks if OpenRouter is unreachable and marks spans with exceptions for observability. Health endpoint is lightweight.
- Graceful shutdown: SIGINT/SIGTERM handlers stop the HTTP server and flush/shutdown the OpenTelemetry SDK to avoid span loss.
- UX polish: inline rename, keyboard shortcuts (F2 to rename, Ctrl/Cmd+Shift+O for new chat, Delete to remove), image upload support, confirmation modal, and sidebar collapse.
- The application automatically generates a conversation title based on the user's first message, providing cleaner session organization without requiring manual naming.
- Better errors: user-facing fallback message when the model is unavailable; traced exceptions for operators.
- Extra tracing detail: spans for cache/history operations and model metadata counts for faster debugging.