English | 中文
api-log-viewer: Svelte trace viewer for LLM proxy logs
api-log-viewer is a Svelte 5 SPA trace viewer for LLM gateway logs. It pairs with api-log to render recorded OpenAI Chat, Anthropic Messages, Responses, and Gemini traffic. Designed as a frontend rather than THE frontend — operators can point it at any compatible JSONL or SQLite-backed trace store. Useful for stacks running sub2api, CLIProxyAPI (CPA), or new-api to inspect requests, responses, SSE chunks, tool calls, reasoning content, sessions, replay data, and WebSocket tunnels.
api-log-viewer 是一个面向 LLM 网关日志的 Svelte 5 SPA trace 查看器。它跟 api-log 配套,渲染录制下来的 OpenAI Chat、Anthropic Messages、Responses、Gemini 流量。设计上是一个前端而不是 THE 前端 —— 后续也可以对接到兼容的 JSONL / SQLite trace 存储。跑 sub2api、CLIProxyAPI(CPA)、new-api 的运维可以用它查看请求、响应、SSE chunk、tool call、reasoning 内容、session、replay 数据与 WebSocket 隧道。
Backend recorder: api-log — Go HTTP recording proxy that captures OpenAI-compatible, Anthropic Messages, Responses, and Gemini gateway traffic into JSONL + SQLite.
Screenshots
Home — aggregate dashboard: capacity, capability badges, active clients (parsed from the User-Agent product token), token-usage sums, traffic volume.
Traces — list view with filter sidebar (status, path with
* prefix, model, key prefix, session, since, limit) + a
keyboard-friendly row table. Selecting a row opens the detail panel.
Trace detail (Overview tab) — request / response identity, tokens in / out, duration, client + key + upstream, content shape (text / reasoning / tool-call counts). Conversation + Raw tabs sit alongside.
Works with api-log
This viewer is a thin reader on top of the api-log HTTP read API. It does
not capture, route, store, or rewrite traffic. All recording is done by
the backend; see ARCHITECTURE.md § 6 in the api-log repo for the read
API contract.
Backend version
- This release tracks
api-log >= 0.1.0. The wire contract (read API + trace JSON shape) is stable across patch versions of that line. - Minor backend bumps may add fields; the viewer ignores unknown fields rather than failing closed.
- WebSocket rendering reads the optional top-level
wsobject (backendARCHITECTURE.md § 3.5). It is present only on backends that record WebSocket upgrades; recorders that predate WS support omit it and the WS surfaces simply don't appear. status: -1is the backend's sentinel for "the upstream never returned response headers" (ARCHITECTURE.md § 3); the client still receives a gateway-generated502. The viewer renders it in the error tone with an explanatory tooltip and exposes it as a filter option; backends that predate the sentinel never emit it.
API base URL
The viewer talks to a single backend origin. In development it proxies
/api/* and /healthz to $APILOG_BACKEND (default
http://127.0.0.1:7862). In production the typical layout is to serve
the static bundle and reverse-proxy /api/* + /healthz from the same
hostname so there is no CORS surface.
Hash-router deployment
Routing is hash-based (#/landing, #/traces, #/traces/<id>). The
bundle is a single SPA — any static file server that returns index.html
for / is sufficient; no rewrite rules are required.
Quick start
Develop locally
pnpm install
pnpm dev
Runs at http://localhost:5180. Vite proxies /api/* and /healthz
to the backend at $APILOG_BACKEND (default http://127.0.0.1:7862).
Browse the UI without a backend
pnpm dev:mock
Starts the same Vite dev server with a dev-only mock of the read API.
It answers /api/traces, /api/traces/:id,
/api/config/{media,retention}, /api/export, /api/media/* and
/healthz from a deterministic in-memory dataset: 130 traces covering
OpenAI Chat (streaming and not), Anthropic Messages with thinking and
tool calls, Responses SSE, Gemini, image generation, a WebSocket tunnel,
multi-turn sessions, a turn whose messages end with harness bookkeeping
elements, upstream 4xx/5xx errors, the -1 no-response sentinel and
cursor pagination.
Authentication still behaves like the real backend: requests need a
bearer token, so paste any non-empty value (for example mock-token)
into the auth modal. The middleware runs before the dev proxy and is
never included in pnpm build. Settings toggles and generated exports
live in memory and reset when the dev server restarts. Set
APILOG_MOCK=1 directly if you prefer to keep using pnpm dev.
Connect to a backend
Start the api-log backend first (see its Quick start), then point the viewer at it:
APILOG_BACKEND=http://127.0.0.1:7862 pnpm dev
On first load the viewer asks for the admin bearer token. The backend
writes that token to ./data/admin_token on its first run; paste it
into the auth modal. It is stored in localStorage (Safari private mode
and sandboxed iframes are tolerated — module load no longer crashes if
storage is unavailable).
Views
Four top-level pages, addressable by hash route:
| Route | Page | What it shows |
|---|---|---|
#/landing |
Home | Backend health, recent trace summary, jump-off links |
#/traces |
Traces | List + filters + detail panel for recorded traces |
#/export |
Export | Bundle a filter set into a downloadable zip for offline analysis |
#/settings |
Settings | Theme, language (en / zh), bearer token, about |
Trace list
#/traces renders the result of GET /api/traces with a sidebar of
filter inputs: status code, path (exact or prefix/*), model,
key_hash, session_root_id, since, limit. Datalist suggestions
populate from observed values in the loaded page.
Trace detail
Selecting a row opens the detail panel with three tabs:
| Tab | Contents |
|---|---|
| Overview | Identity, status, latency, model, tool-call summary, reasoning summary, project context extracted from AGENTS.md / CLAUDE.md if recorded |
| Conversation | Rendered messages — system / user / assistant / tool blocks, SSE replay, tool-call args + results |
| Raw | Request headers + body and response headers + body co-located, single tab so operators stop ping-ponging |
Headers and Body were folded into the Raw tab to keep diff-style
reading in one place. SSE replay is rendered inside Conversation rather
than as a separate tab.
Streamed responses have no resp.body. When a trace carries
resp.events, the Raw tab's response body section replays those frames:
a structured list by default (event name, relative time, payload
preview, expand to a JSON tree or text), with the raw text toggle
reconstructing normalized SSE wire text. The section hint reports the
frame count and reconstructed size.
Traces recorded from a WebSocket upgrade have no HTTP bodies: the
Conversation tab renders the frame tunnel (direction, opcode, length,
inter-frame delta, JSON / text / binary payload, truncation flags), the
Overview tab summarises frame and byte totals plus the close handshake,
and the Raw tab dumps the full ws object.
Export
The Export page lets the operator apply a trace filter and download a zip bundle (traces + matching JSONL + any project-context files), so a downstream agent can ingest the bundle offline.
Deploy
Static build
pnpm build
# dist/ contains the static SPA bundle
The build produces a single bundle (one HTML + hashed JS/CSS). Bundle size is auto-measured in CI at roughly 142 KB JS gzipped — small enough that aggressive CDN caching is the only required performance work.
Backend-hosted bundle
The api-log binary can serve a pre-built bundle itself; it never downloads
viewer assets. Point its viewer.local_path (or
APILOG_VIEWER_LOCAL_PATH) at this repo's dist/ directory:
viewer:
local_path: /opt/api-log-viewer/dist
/viewer/ then serves the SPA, and viewer.public_path
(APILOG_VIEWER_PUBLIC_PATH) changes the prefix. When local_path is
unset or has no index.html, /viewer/ returns 503 viewer_unavailable
while the read API keeps serving. The bundle is built with base: './'
and hash routing, so one dist/ tree works at the prefix and at the host
root; the read API stays at <origin>/api/* on the same origin.
Tag builds attach dist.zip to the release — unzip it into dist/ to
stage viewer.local_path without a local toolchain.
Docker image
The repo ships a multi-stage Dockerfile that builds the
SPA and serves it with Caddy. The container is self-contained: Caddy
serves the SPA from / and reverse-proxies /api/* + /healthz to the
api-log backend on that same origin — exactly what the SPA's
connect-src 'self' CSP requires. You only point it at a backend:
docker run --rm -p 8080:80 \
-e APILOG_BACKEND=http://api-log:7862 \
git.o0kam1.dev/humanlesscode/api-log-viewer:latest
Published builds live on the project registry:
git.o0kam1.dev/humanlesscode/api-log-viewer:latest and
git.o0kam1.dev/humanlesscode/api-log:latest for the backend. Pulling is
anonymous (no docker login), the tag is latest, and both images are
linux/amd64 only. To build the viewer yourself instead, run
docker build -t api-log-viewer . and use that tag.
APILOG_BACKEND defaults to http://127.0.0.1:7862; a bare
host:port (no scheme) is accepted too. Open http://localhost:8080 and
paste the admin token into the viewer's auth modal.
Compose, next to api-log on a shared network:
services:
api-log:
image: git.o0kam1.dev/humanlesscode/api-log:latest
volumes: ["./api-log-data:/data"]
expose: ["7862"]
viewer:
image: git.o0kam1.dev/humanlesscode/api-log-viewer:latest
ports: ["8080:80"]
environment:
APILOG_BACKEND: http://api-log:7862
api-log needs no published port — the viewer reaches it over the compose
network. Because the container proxies the read API itself, the
reverse-proxy samples below are only needed for non-Docker deployments or
when you already run an edge proxy (e.g. for TLS).
The Caddyfile and the APILOG_BACKEND normalizer live in
deploy/caddy/; build-context filters in
.dockerignore.
Caddy reverse proxy
Serve the static bundle and reverse-proxy the backend on the same origin to avoid CORS:
your.domain {
root * /opt/api-log-viewer/dist
file_server
handle /api/* { reverse_proxy 127.0.0.1:7862 }
handle /healthz { reverse_proxy 127.0.0.1:7862 }
}
Nginx reverse proxy
server {
listen 443 ssl;
server_name your.domain;
root /opt/api-log-viewer/dist;
index index.html;
location / {
try_files $uri /index.html;
}
location /api/ { proxy_pass http://127.0.0.1:7862; }
location /healthz { proxy_pass http://127.0.0.1:7862; }
}
Security notes
- The admin bearer token is held in
localStorage. Anyone with DOM access to the viewer origin can read it; treat the viewer origin as trusted and gate it (mTLS, VPN, basic auth at the reverse proxy) on shared deployments. - The viewer never re-issues recorded traffic to a real upstream. The Conversation tab's SSE replay is a render of recorded bytes, not a live request.
- The viewer does not perform redaction. What it shows is what the
backend recorded; see api-log's
SECURITY.mdfor the capture-side redaction posture.
Compatibility
| Component | Version |
|---|---|
| api-log backend | >= 0.1.0 |
| Node | >= 22 (CI matrix: 22.x) |
| pnpm | >= 9 |
| Browsers | Last two versions of Chrome, Firefox, Safari |
Bundle size
CI measures gzipped JS size on each build. The current bundle is approximately 142 KB JS gzipped, 141 source files, single-bundle SPA.
Development
pnpm install
pnpm i18n # compile messages/*.json into src/paraglide/
pnpm check # svelte-check(runs i18n first)
pnpm test # vitest unit tests(pnpm test:watch 监听)
pnpm build # production bundle into dist/
pnpm dev # dev server on :5180
CI runs check + test + build on push and pull request; release tags
on v* build artifacts. See .github/workflows/ci.yml.
Internals:
- Svelte 5 (runes) + Vite + TypeScript.
- Styling is hand-written CSS; routing is hash-based.
- i18n: ICU MessageFormat catalogs in
messages/{en,zh}.json, compiled by Paraglide (build-time,src/paraglide/is generated and gitignored).t()insrc/lib/i18n.svelte.tswraps the compiled messages, so keys and parameters are type-checked and a language switch re-renders in place without a reload;src/lib/intl.tsformats numbers with the same language, andsrc/lib/format.tsrenders timestamps with fixed ISO-style patterns. - All backend calls go through
src/lib/api.ts, which handles bearer injection and the 401 → auth-modal flow.
Related projects
- api-log — Go recording
proxy this viewer reads from. The HTTP read API contract lives in
ARCHITECTURE.md § 6of that repo. SECURITY.md— viewer-side threat model.CHANGELOG.md— release notes.
Acknowledgements
Backend + design influence
api-log— every surface in this viewer renders something the backend already recorded; the read API contract drives the shape of every page.- Svelte 5 (runes) — module-level
$statecells made the desync-prone i18n / theme / auth state surfaces tractable without a store library.
Development assistance
This codebase and its documentation were developed with Claude Opus 4.8 (Anthropic) as the primary pair-programmer for the Phase L UI revamp, the i18n dictionary, the localStorage-hardening pass, and these READMEs; and GPT-5.5 via Codex CLI as a research and review assistant — pre-release adversarial review, README structural analysis against reference OSS projects (CLIProxyAPI, sub2api, cc-switch), and tone audits. The cut / keep / amend judgments are the human author's; AI assistance is named here for transparency, not as authorship.
License
MIT — Copyright 2026 Leo Yun.


