English | 中文

api-log-viewer: Svelte trace viewer for LLM proxy logs

Source License: MIT Bundle size

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.

Home — aggregate dashboard

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.

Traces — list + filter sidebar

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.

Trace detail — Overview tab

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 ws object (backend ARCHITECTURE.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: -1 is the backend's sentinel for "the upstream never returned response headers" (ARCHITECTURE.md § 3); the client still receives a gateway-generated 502. 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.md for 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() in src/lib/i18n.svelte.ts wraps the compiled messages, so keys and parameters are type-checked and a language switch re-renders in place without a reload; src/lib/intl.ts formats numbers with the same language, and src/lib/format.ts renders 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.
  • api-log — Go recording proxy this viewer reads from. The HTTP read API contract lives in ARCHITECTURE.md § 6 of 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 $state cells 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.

Languages
TypeScript 49.6%
Svelte 49%
CSS 0.8%
Dockerfile 0.4%
Shell 0.1%
Other 0.1%