memoryelaine is a single-binary Go middleware proxy for OpenAI-compatible inference APIs. It sits transparently between clients and one fixed upstream provider. Its primary purpose is to proxy requests with no intentional buffering of active streams while asynchronously logging selected request/response pairs, timings, and HTTP metadata to a local SQLite database.
AI DISCLOSURE 🤖: Almost all source code in this repository has been generated via (back-and-forth) interactions with LLMs (large language models).
# Copy the example config
cp example-config.yaml config.yaml
# Build and test
CGO_ENABLED=1 GOOS=linux go build -tags sqlite_fts5 -o memoryelaine .
# Run the proxy
./memoryelaine serve --config ./config.yamlOnce the proxy is running:
- Send client traffic to
proxy.listen_addr - Browse the management UI on
management.listen_addr - Inspect logs with
memoryelaine logormemoryelaine tui - ...or from Emacs via ./emacs-memoryelaine/
demo-webui.mp4
demo-emacs-gui.mp4
demo-tui.mp4
demo-cli.mp4
Start both HTTP servers:
- Proxy listener: forwards client traffic to the configured upstream. Requests matching
proxy.log_pathsare captured. - Management listener: Web UI, JSON API, Prometheus metrics, health.
The management surface also exposes a runtime recording switch. When recording is paused, matching proxy paths are still forwarded normally, but no new request/response bodies are captured to SQLite.
Query stored logs from the command line.
Usage:
-f, --format json|jsonl|table Output format (default: json)
-n, --limit INT Number of records to return (default: 20)
--offset INT Pagination offset (default: 0)
--status INT Exact HTTP status filter, e.g., 200 or 500
--path STRING Exact request path filter
--since VALUE RFC3339 timestamp or relative duration (e.g., 30m, 2h, 7d)
--until VALUE RFC3339 timestamp or relative duration
-q, --query STRING Substring search across req_body and resp_body
--id INT Return a single log record by primary keyExamples:
memoryelaine log -f table -n 10
memoryelaine log --status 500 --since 24h
memoryelaine log --path /v1/chat/completions -q tool_call
memoryelaine log --id 42Open the interactive terminal UI for browsing logs.
Keybindings:
j/kor arrows: Navigate the table or scroll the detail viewenter: Open detail view for the selected rowescorq: Leave detail viewv: Toggle stream view mode (Raw / Assembled). Assembled is the default when available.z: Toggle reasoning section fold state in assembled moder: Refresh current pagen/p: Next / previous pagef: Cycle exact status filters (none → 200 → 400 → 500)xthenb/B/c/R: Export request raw, response raw, assembled content, or assembled reasoning (TUI prompts for a save path)q/ctrl+c: Quit
Delete records older than N days.
Examples:
memoryelaine prune --keep-days 7 --dry-run
memoryelaine prune --keep-days 30 --vacuumExample config file (config.yaml):
proxy:
listen_addr: "0.0.0.0:8687"
upstream_base_url: "https://api.openai.com"
timeout_minutes: 23
log_paths:
- "/v1/chat/completions"
- "/v1/completions"
management:
listen_addr: "0.0.0.0:8677"
auth:
username: "admin"
password: "changeme"
database:
path: "./memoryelaine.db"
logging:
max_capture_bytes: 8388608
level: "info"Lookup order: --config <path> → ./config.yaml → $HOME/.config/memoryelaine/config.yaml → Built-in defaults.
Address for the proxy listener. Default: 0.0.0.0:8687
Base URL for the single upstream provider. Must be a valid http:// or https:// URL. Default: https://api.openai.com
Connection setup / response-header timeout budget. This does not terminate an already-active response stream. Default: 23
Exact path allowlist for payload capture. Requests on other paths are still proxied, just not written to SQLite. Default:
/v1/chat/completions/v1/completions
Address for the management server. Must differ from proxy.listen_addr. Default: 0.0.0.0:8677
Maximum bytes returned in body preview responses via /api/logs/{id}/body. Default: 65536 (64 KiB)
Basic Auth username for /, /api/logs, /api/logs/{id}, /api/logs/{id}/body, and /metrics. Default: admin
Basic Auth password for the management endpoints above. Default: changeme
Path to the SQLite database file. Default: ./memoryelaine.db
Maximum number of request or response body bytes retained in memory and persisted in the database per direction. Bodies larger than this are truncated in the log entry while still being fully streamed to the client. Must be greater than zero. Default: 8388608 (8 MiB)
Structured log verbosity for the service process. Accepted values: debug, info, warn, error. Default: info
GET /- Embedded Web UI (Basic Auth protected)GET /api/logs- Log summaries (no bodies/headers). Supportsquery,limit,offsetparameters. (Basic Auth protected)GET /api/logs/{id}- Log detail metadata with decoded headers and stream-view availability. No bodies. (Basic Auth protected)GET /api/logs/{id}/body- Request or response body content. Params:part(req|resp, default: resp),mode(raw|assembled, default: raw),section(all|content|reasoning; assembled response only),full(true|false, default: false). (Basic Auth protected)GET /api/recording- Current runtime recording state (Basic Auth protected)PUT /api/recording- Change runtime recording state with{"recording":true|false}(Basic Auth protected)GET /last-request- Latest captured request body (Basic Auth protected)GET /last-response- Latest captured response body (Basic Auth protected)GET /metrics- Prometheus metrics (Basic Auth protected)GET /health- Public health JSON, no auth required. Includes the currentrecordingstate.
GET /api/logs accepts a query parameter with a DSL string (see below), plus limit (integer, max 1000) and offset (integer). When query is absent, legacy parameters are accepted as fallback: status, path, q, since, until.
The query parameter accepts a search string combining free-text and structured filters:
- Bare words: full-text search (FTS5) across request and response bodies
status:200orstatus:4xx— filter by status code or wildcard rangemethod:POST— filter by HTTP methodpath:/v1/chat/completions— filter by request pathsince:1horsince:2024-01-01T00:00:00Z— entries after timeuntil:24horuntil:2024-01-01T00:00:00Z— entries older than timeis:error,is:req-truncated,is:resp-truncated— flag filtershas:req,has:resp— body presence filters-status:500— negate any filter"exact phrase"— quoted phrase search
Example: status:2xx method:POST path:/chat hello world
management.preview_bytes: max bytes returned in body preview (default: 65536)
When one or more loggable requests have been proxied while recording is paused, /last-request and /last-response keep serving the last captured bodies but label them as stale until a newly captured body replaces them.
When viewing a streamed response in the detail view, the TUI, Web UI, and Emacs client offer a Stream View toggle:
- Raw: the exact stored response body, including SSE framing
- Assembled: reconstructed content derived from the SSE stream, with separate content and reasoning sections when available
Assembled mode is currently supported for:
/v1/chat/completions/v1/completions
Assembled mode is unavailable for truncated, non-streamed, or unsupported responses. When parsing only partially succeeds, the recovered text is shown with a warning indicator.
./scripts/build-and-test.sh
./scripts/run-lint-checks.shIf you see an error reading: migrating database: executing FTS schema: creating FTS table: no such module: fts5
Try building with fts5 enabled:
CGO_ENABLED=1 GOOS=linux go build -tags sqlite_fts5 ...
CGO_ENABLED=1 GOOS=linux go run -tags sqlite_fts5 . serve --config ./example-config.yaml
GOFLAGS="-tags=sqlite_fts5" go mod tidySpecifications, implementation plans etc. are found under design-docs-wip/, design-docs-main/, and design-docs-legacy/. The -wip folder is "work in progress" (should typically be empty when we are on main branch), the -main folder should typically describe the state of the main-branch / "what's released", the -legacy folder is typically of little interest and is to be considered a historical legacy and typically contains documents that are out-of-date.