┌─ openai dialect ─┐ ┌─ anthropic ─┐
│ │ ──────▶ │ │
│ /v1/chat/... │ ◀────── │ /messages │
└──────────────────┘ └──────────────┘
│ │
└──── full audit ────────┘
on disk
OpenAI-compatible gateway for Claude Max. Speaks the dialect. Logs every byte. Ships the dashboard.
Run it · API · Dashboard · Plaintext reasoning · Architecture · Disclaimer
Nothing you couldn't build yourself in a weekend, except we spent about twenty commits tracking down one specific server behaviour so you don't have to.
Three things it does, in this order of importance:
- Speaks the OpenAI dialect, so your existing tools just work.
- Logs every byte of every call — client body, transformed upstream body, raw SSE stream, timings, tokens, reasoning. Everything. On disk.
- Ships a dashboard that treats an LLM call as a first-class object: readable, searchable, replayable.
This gateway authenticates with Anthropic using your Claude Code OAuth credentials. Anthropic's Terms of Service state those tokens are for official clients. This project is not an official client — it is a community workaround, and a pragmatic one.
Anthropic can break it tomorrow by changing the OAuth flow or the billing signature contract. We have already seen them tighten screws around thinking redaction mid-2026. If one day the gateway stops working, it is not you, it is the moving ground.
Use at your own discretion. Do not put this behind a product you charge money for.
| Runtime | Bun, latest stable |
| Credentials | Authenticated Claude Code install (~/.claude/.credentials.json, override with CREDENTIALS_PATH) |
| Disk | A few hundred MB if you log heavily; SQLite WAL grows with traffic |
| Network | Outbound HTTPS to api.anthropic.com |
bun install
bun run src/index.ts # port 3456
bun run src/index.ts 3457 # overrideThe backend serves the prebuilt dashboard from src/ui/dist/. If no build
exists, GET / returns a 503 telling you to build — see Build.
OpenAI-compatible surface, plus a telemetry surface that exposes the audit log over HTTP. Point any existing OpenAI client at the base URL and it works.
| Endpoint | Scope | Purpose |
|---|---|---|
GET /health |
platform | liveness |
GET /v1/models |
OpenAI-compat | upstream catalog with derived effort variants |
POST /v1/chat/completions |
OpenAI-compat | streaming and non-streaming chat |
POST /v1/tokens/count |
OpenAI-compat | token count for a message set |
GET /api/account/profile |
gateway | cached OAuth profile |
GET /api/telemetry/requests |
telemetry | recorded requests, filterable |
GET /api/telemetry/requests/:traceId |
telemetry | single request with body and SSE events |
GET /api/telemetry/logs |
telemetry | raw event log |
GET /api/telemetry/stream |
telemetry | SSE live feed of new events |
GET /api/telemetry/metrics |
telemetry | aggregated metrics for a window |
GET /api/telemetry/export |
telemetry | CSV or JSON export |
curl -s "http://127.0.0.1:3456/api/telemetry/requests?limit=5" \
| jq '.requests[] | {traceId, model, duration, inputTokens, outputTokens}'The SQLite store at logs/telemetry.db is also directly queryable. No
abstraction to learn, no ORM to fight.
All routes are URL-driven and shareable. A dashboard without keyboard nav is cosplay, so this one has it.
| Route | Contents |
|---|---|
/ |
requests list with filters, keyboard nav, pagination |
/sessions |
conversations grouped from consecutive turns |
/s/:sessionId |
all turns of a conversation, sticky per-turn header |
/r/:traceId |
full transcript, technical panel, span timeline, replay, export |
/live |
SSE event stream, pausable, level and stream filters |
/metrics |
requests, latency, errors, tokens — window toggle 1m / 5m / 1h / 24h |
/compare?a=X&b=Y |
two transcripts side by side with scroll-sync |
| Key | Action |
|---|---|
/ |
focus search |
j k |
move row selection |
Enter |
open selected |
Esc |
clear / back |
client gateway upstream
──────── ────────── ───────────
┌──────────┐
OpenAI ───POST───▶ │transform │ ──POST──▶ Anthropic
client /v1 │ openai → │ /v1 API
│ anthropic│
└─────┬────┘
│ every request,
│ every byte,
│ every SSE event
▼
┌──────────┐
│ SQLite │ ─read─▶ Dashboard
│ telemetry│ Vite SPA
│ .db │ Live · Replay · Compare · Export
└──────────┘
Backend (src/) is a Bun native HTTP server. Stateless apart from the SQLite
event store and an in-memory credential cache.
| Path | Concern |
|---|---|
src/http/ |
routing, static, middleware |
src/transform/ |
OpenAI ↔ Anthropic translation (request and response) |
src/upstream/ |
Anthropic client, headers, billing, count-tokens |
src/observability/ |
event bus, SQLite store, tracer, logger |
src/domain/ |
account, credentials, models, tool-mapping |
src/ui/ |
Vite + React 19 SPA — separate sub-project |
Frontend is Vite + React 19 + TanStack Router (file-based) + TanStack Query + Tailwind v4 + shadcn/ui. Builds to a static SPA served by the backend on the same port. No CORS dance, no separate deploy, no nginx config.
Two terminals from the repo root:
# backend
bun run src/index.ts 3457
# UI
cd src/ui
bun install
bun run dev # http://localhost:5173Vite proxies /api, /v1, and /health to http://127.0.0.1:3457. HMR is on.
cd src/ui
bun run build # tsr generate && tsc -b && vite build → src/ui/dist/The backend picks up the bundle automatically on the next request.
bun test # full backend suite — 195 tests
bunx tsc --noEmit # backend typecheck — 0 errors
cd src/ui && bun run typecheckThe repo follows Strict TDD for behavioural changes (see CLAUDE.md). bun test is the merge gate.
| Env | Default | Notes |
|---|---|---|
PORT |
3456 |
first CLI arg overrides |
BIND_HOST |
127.0.0.1 |
Defaults to loopback so the proxy is not exposed by accident. Set to 0.0.0.0 (or a specific IP) only if you knowingly want to expose the service — remember this gateway authenticates to Anthropic with your OAuth token. |
CREDENTIALS_PATH |
~/.claude/.credentials.json |
OAuth credentials source |
Anthropic exposes two different contracts for thinking on the same endpoint, and the documentation does not make the distinction obvious. They are not interchangeable.
Send thinking: { type: "enabled", budget_tokens: N } and the server assumes
you intend to re-inject the ciphertext signature on the next turn. You get
back an empty thinking block shell and a signed blob that is opaque to anyone
without Anthropic's private keys. Private compute. Great for multi-turn agent
frameworks that want opacity. Useless for an audit pipeline where you want to
actually read what the model was thinking.
Send thinking: { type: "adaptive", display: "summarized" } together with
output_config: { effort } and the server emits thinking_delta events
containing the model's reasoning in plaintext. It is summarized — not the raw
internal monologue — but it is readable, and it matches what the official
Claude Code CLI and the OpenCode anthropic plugin emit on the wire.
This gateway picks the second form. That is the entire unlock. Dozens of commits of investigation, one field difference, one lesson learned: when you claim byte-for-byte parity with another client, verify it with a real wire capture. Reading their source is not the same thing.
Not production-ready in the enterprise sense. Not audited for security. Not supported by Anthropic. Not multi-tenant — it reads credentials from disk and uses them. Not a replacement for a real API key if your workload needs SLA.
It is a tool for people who want to see, in full colour, what their LLM is doing on a Claude Max subscription, today.
| File | Contents |
|---|---|
OBSERVABILITY.md |
event model, SQLite schema, API surface, retention |
CLAUDE.md |
agent conventions for this codebase (Bun-first rules) |
LICENSE |
MIT |
Built with Bun · TypeScript · React 19 · TanStack · shadcn/ui · SQLite