Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# CLAUDE.md

Guidance for Claude Code and contributors working in this repository.

## Project Overview

AgentLedger is a Go reverse proxy that sits between AI agents and LLM providers. It
meters token usage per request, calculates costs, enforces daily/monthly budgets, and
exposes cost observability — all without requiring application code changes (agents
just point their SDK's base URL at the proxy). Module path:
`github.com/WDZ-Dev/agent-ledger`.

## Architecture

Request flow (all wired in `cmd/agentledger/serve.go` → `runServe`):

1. `internal/proxy` (`Proxy.ServeHTTP`) is the core `httputil.ReverseProxy`. It reads the
request body, detects the provider, and runs pre-flight checks in order: blocklist →
agent loop detection → rate limit → tenant resolution → budget check → pre-flight cost
estimation (rejects if worst-case cost from `max_tokens` exceeds remaining budget).
2. Request metadata and start time are stashed in the request `context.Context` (typed
`ctxKey` constants), then the request is forwarded upstream via `rewrite` (rewrites URL
to the provider upstream, strips provider path prefix and agent headers).
3. `modifyResponse` parses the upstream response. Streaming (`text/event-stream`) responses
are wrapped by `internal/proxy/streaming.go` (`newStreamInterceptor`) to accumulate token
usage from SSE chunks; non-streaming responses are parsed directly.
4. Cost is computed by `internal/meter` (`Meter.Calculate`), a `ledger.UsageRecord` is built
and handed to the async `ledger.Recorder`, and the agent session cost is updated.

Key packages under `internal/`:

- `provider` — per-provider `Provider` interface (`Name/Match/ParseRequest/ParseResponse/
ParseStreamChunk/UpstreamURL`). `Registry.Detect` picks a provider by matching the request.
OpenAI-compatible providers (Groq, Mistral, DeepSeek, xAI, Perplexity, Together, Fireworks,
OpenRouter, Cerebras, SambaNova) share `openai_compat.go` and route by path prefix (e.g.
`/groq/`). Also holds API-key extraction/hashing and agent-header helpers.
- `meter` — pricing table (`pricing.go`, `DefaultPricing`) + cost calc with exact-then-
longest-prefix model matching; `estimator.go` uses tiktoken to estimate tokens when the API
omits usage.
- `ledger` — storage. `Ledger` interface implemented by SQLite (`sqlite.go`, default) and
Postgres (`postgres.go`). Migrations are `//go:embed`ed from `migrations/{sqlite,postgres}`
and run via goose. `Recorder` is an async buffered write pipeline (drops on overflow).
- `budget` — `Manager.Check` returns Allow/Warn/Block; `circuit_breaker.go` provides
`BreakerTransport` (a `http.RoundTripper`) for upstream failure protection.
- `agent` — session tracking (`Tracker`), loop detection and "ghost agent" detection
(`detector.go`). Sessions persisted via the `SessionStore` interface (implemented by the
storage backends).
- `mcp` — Model Context Protocol metering: HTTP proxy (`httpproxy.go`) and a stdio wrapper
(`stdio.go`, driven by the `mcp-wrap` CLI command) that intercepts JSON-RPC `tools/call`.
- `tenant`, `ratelimit`, `alert` (Slack/webhook notifiers, rate-limited + multi), `admin`
(token-auth admin API + API-key blocklist), `otel` (OpenTelemetry → Prometheus metrics),
`dashboard` (embedded web UI + REST API).

## Key Concepts

- **UsageRecord** (`internal/ledger/models.go`) — one metered LLM call: provider, model,
hashed API key, token counts, cost, duration, status, and agent/session/user/tenant IDs.
- **API key hashing** — keys are never stored raw; `provider.HashAPIKey` fingerprints
prefix+suffix via SHA-256.
- **Agent headers** — `X-Agent-Id`, `X-Agent-Session`, `X-Agent-User`, `X-Agent-Task`,
`X-Agent-Session-End` (query-param fallbacks `?_agent_id=` etc.); stripped before upstream.
- **Estimated cost** — records are flagged `Estimated` when the model has no known pricing.

## Commands

Use the Makefile (Go is not always on PATH; targets assume tools under `~/go/bin`):

- `make build` — compile to `bin/agentledger` (or `go build ./cmd/agentledger`).
- `make dev` — build + run `serve` with `configs/agentledger.example.yaml`.
- `make test` — `go test -race -cover -count=1 ./...`.
- `make test-short` — fast tests only (`-short`).
- `make test-coverage` — HTML coverage report.
- `make lint` / `make lint-fix` — golangci-lint (v2).
- `make fmt` — `gofmt` + `goimports -local github.com/WDZ-Dev/agent-ledger`.
- `make vet`, `make vulncheck` (govulncheck), `make tidy`.
- `make check` — `fmt vet lint test vulncheck` (mirrors CI in `.github/workflows/ci.yml`).
- `make setup` — install dev tools (golangci-lint, lefthook, govulncheck, goimports) + git hooks.

CLI subcommands (`cmd/agentledger`): `serve`, `costs` (`--last`, `--by`, `--tenant`),
`export` (`--format csv|json`), `mcp-wrap -- <cmd>`, `version`, `healthcheck` (hidden).

## Conventions

- Standard Go layout: entrypoint/CLI in `cmd/agentledger`, all logic in `internal/`
packages (not importable externally). One responsibility per package; `_test.go` beside
each source file.
- Errors wrapped with `fmt.Errorf("...: %w", err)`; error vars named `ErrFoo` (`errname`).
- Structured logging with `log/slog` throughout; a `*slog.Logger` is passed into constructors.
- Optional features are wired by passing `nil` (e.g. proxy accepts `nil` budget/tracker/
metrics/limiter to disable them) and gated via `Enabled()` methods.
- IDs are ULIDs (`oklog/ulid/v2`). Linters enforced: govet, errcheck, staticcheck, gosec,
revive, goconst, bodyclose, and more (see `.golangci.yml`). Go 1.25.

## Configuration

Loaded via Viper in `internal/config/config.go` (`Load`). Sources, in order: config file →
environment variables → built-in defaults.

- **Config file**: passed with `-c/--config`, else auto-discovered as `agentledger.yaml` in
`.`, `./configs`, `$HOME/.config/agentledger`, `/etc/agentledger`. See
`configs/agentledger.example.yaml`.
- **Environment variables**: prefix `AGENTLEDGER_`, dots → underscores (e.g.
`AGENTLEDGER_LISTEN`, `AGENTLEDGER_STORAGE_DSN`, `AGENTLEDGER_STORAGE_DRIVER`,
`AGENTLEDGER_ADMIN_TOKEN`).
- **Key settings**: `listen` (default `:8787`); `storage.driver` (`sqlite` default, or
`postgres`) + `storage.dsn`; `providers.*` (OpenAI/Anthropic enabled by default, extras
disabled); `budgets`, `rate_limits`, `agent` (loop/ghost thresholds), `tenants`, `alerts`
(Slack `webhook_url` / webhooks), `admin` (`enabled` + `token`), `mcp` (`enabled` +
`upstream`), `dashboard.enabled` (default true), `cors.allow_origins`, `tls.cert_file`/
`key_file`, `circuit_breaker`, `recording.buffer_size`/`workers`, `log.level`/`format`.
- **`mcp-wrap`** reads agent context from `AGENTLEDGER_AGENT_ID`, `AGENTLEDGER_SESSION_ID`,
`AGENTLEDGER_USER_ID`, `AGENTLEDGER_TASK`.

No required env vars for a default run (`agentledger serve` works with SQLite defaults).
Loading