A bring-your-own-key chat client for any OpenAI-compatible /v1/chat/completions API. Live at chat.4x.rip.
Your endpoints, API keys, and chats live in your browser. The server is only involved in three optional things: GitHub login, cross-device chat sync, and two small proxies (OpenRouter model metadata, Exa search).
- Any OpenAI-compatible endpoint — presets for OpenRouter, OpenAI, Anthropic, Google, Groq, Together, Mistral, OpenCode Zen, Ollama, and LM Studio, plus multiple saved endpoint profiles (name, base URL, key, default model). Most are called browser-direct; OpenCode Zen has no CORS headers so it routes through the worker (key transits per request, never stored or logged — flagged in the UI).
- Streaming responses over SSE with a stop button, plus a graceful fallback for servers that don't stream.
- Markdown rendering with syntax-highlighted code blocks (copy button), GFM tables, and KaTeX math.
- Multi-model compare — pick 2+ models, watch them stream side by side, then promote one response to continue the thread.
- Attachments — images sent as multimodal content parts; text files inlined into the prompt.
- Web search via Exa using your own Exa key, with cited sources under the response. Models that support tool calling get it as a
web_searchtool and decide when to search; other models get results injected up front. Afetch_urltool (also Exa-backed) lets the model pull the live full text of a specific page when you paste a link. - Artifacts — tool-capable models can build small websites and apps right in the chat:
create_artifact/edit_artifactrender a live preview in a sandboxed side panel (no access to your keys), with reload, open-in-tab, and download. Anask_usertool lets the model pause mid-generation and ask clarifying questions with one-click options. Agentic chains run up to 12 tool rounds (think → ask → build → edit → answer). - E2B sandboxes — add an E2B key to give tool-capable models three more powers: code execution (
run_code/run_command/ file tools) in a persistent cloud sandbox — real Python, installs, chart outputs; build real apps (build_artifact) — the model writes a multi-file React/TypeScript project, which is bundled with esbuild in the sandbox (real npm deps) into one self-contained page shown in the preview panel, with a Preview / Code toggle to browse the source tree; and computer use (computer_start/computer_action) — a virtual Linux desktop the model drives while you watch the live stream, with a Stop-sandbox button. Computer tools require a vision-capable model. Sandboxes are torn down on stop, tab close, or E2B's own timeout. - MCP servers — connect remote Model Context Protocol servers (Streamable HTTP transport, CORS required); their tools are offered to tool-capable models automatically. Servers that require OAuth are supported end-to-end in the browser: metadata discovery, dynamic client registration, PKCE popup flow, and silent token refresh — tokens stay local like every other key.
- Chat ergonomics — edit + resend, regenerate with version history, rename/delete,
⌘Ksearch across chats,⌘⇧Onew chat. - System prompts & sampling — a global default system prompt plus per-conversation overrides for prompt, temperature, max tokens, model, and endpoint.
- Local-first storage — everything in IndexedDB; the app fully works with zero login.
- Optional sync — sign in with GitHub and opt in to sync chats to D1 (attachments to R2, capped at 8MB each). Last-write-wins per conversation. Endpoints and API keys are never synced.
- Endpoint profiles and API keys are stored in
localStorageonly. They are never sent to this app's server, never synced, never logged. - Model API calls go directly from your browser to the endpoint you configured.
- The Exa key transits our worker per-request (Exa's API blocks direct browser calls) but is not stored or logged server-side.
- The E2B key goes directly from your browser to E2B (their API allows it); sandbox usage is billed to your E2B account.
- GitHub login is identity-only: the OAuth token is revoked immediately after fetching your profile.
- Cloudflare Worker + Hono serves the SPA (via the
ASSETSbinding) and the small API: GitHub OAuth,/api/sync/*(D1 + R2),/api/openrouter/models(edge-cached slim metadata),/api/exa/search(pass-through proxy). - Vite + React 19 + TypeScript, Tailwind v4 (CSS-first config), shadcn/ui (
base-novastyle on Base UI), TanStack Query for server state, Dexie (useLiveQuery) as the single source of truth for chats — streams write through to IndexedDB, so generation survives navigation. - D1 stores users, conversation metadata, and per-message rows; R2 stores attachment blobs.
-
Create a GitHub OAuth app (only needed for login/sync): callback URL
http://localhost:5173/api/auth/callback. -
Copy the env template and fill it in:
cp .dev.vars.example .dev.vars
-
Install and run (the Vite dev server runs the SPA and the Worker together on one origin):
pnpm install pnpm exec wrangler d1 migrations apply chat --local pnpm dev
| Name | Kind | Purpose |
|---|---|---|
GITHUB_CLIENT_ID |
var (wrangler.jsonc) |
GitHub OAuth app client id |
APP_BASE_URL |
var (wrangler.jsonc) |
Origin used in the OAuth callback URL |
GITHUB_CLIENT_SECRET |
secret | GitHub OAuth app client secret |
COOKIE_SECRET |
secret | AES key for the encrypted session cookie |
Locally these come from .dev.vars (gitignored).
One-time setup:
pnpm exec wrangler d1 create chat # put the database_id in wrangler.jsonc
pnpm exec wrangler r2 bucket create chat-media
pnpm exec wrangler d1 migrations apply chat --remote
pnpm exec wrangler secret put GITHUB_CLIENT_SECRET
pnpm exec wrangler secret put COOKIE_SECRET # e.g. openssl rand -base64 32Then set GITHUB_CLIENT_ID in wrangler.jsonc (prod OAuth app callback: https://your-domain/api/auth/callback) and deploy:
pnpm run deploypnpm dev— Vite dev server (SPA + Worker, one origin)pnpm build— typecheck app + worker, then buildpnpm preview— preview the production buildpnpm run deploy— build andwrangler deploypnpm cf-typegen— generate Worker binding types