Skip to main content

Local LLM API proxy — captures usage, attributes cost to projects.

Project description

Halton Meter

Local LLM API proxy that captures usage and attributes cost to projects.

Halton Meter is a governance and cost-attribution tool for LLM API spend. It runs a local proxy that observes outbound traffic to LLM providers (Anthropic, OpenAI, Gemini, Groq, etc.), logs every request with project-level attribution, computes accurate cost from published pricing, and surfaces it in a dashboard.

Designed for solo developers, agencies, and in-house dev teams who use Claude Code, the Anthropic SDK, or other LLM clients heavily and want transparency over what's being spent and on what.

Quickstart

The exact happy-path sequence on a clean macOS machine:

pipx install halton-meter
halton-meter init                  # plug-and-play; macOS will prompt for admin password
halton-meter run claude            # meter Claude Code's API calls
halton-meter report                # see captured rows

That's it. halton-meter init installs the local CA cert (a macOS dialog will pop up asking for your password), the launchd supervisor entry, and the port allocations. By default, only commands you explicitly route via halton-meter run <cmd> are metered — browsers, GUI apps, and Spotlight- launched terminals are untouched, so init cannot break HSTS browsing.

Already used a previous version? halton-meter doctor prints a top-line verdict (HEALTHY / INCONSISTENT / BROKEN) plus copy-pasteable next steps.

To extend coverage:

halton-meter init --apps           # cover terminals + Spotlight-spawned apps
                                   # (no system proxy — cannot break browsing)
halton-meter init --full           # cover everything: browsers, GUI apps,
                                   # terminals (system proxy + launchctl env)

(--gui is kept as a deprecated alias for --full.)

Requires Python 3.11+.

What if halton-meter init doesn't succeed?

Most failures are caught by the post-init self-test, which rolls the install back automatically and exits non-zero. Common cases:

Symptom Cause Action
The macOS admin dialog appears, asks for your password Expected — the cert-trust step needs admin rights Type your account password and click OK. This is the same dialog Brew, the macOS installer, etc. use.
--non-interactive cannot elevate ... (exit 2) Both osascript and pre-cached sudo are unavailable Either run halton-meter init interactively (the GUI dialog will appear) OR pre-cache with sudo -v && halton-meter init --non-interactive.
Default port 8080 / 8765 is busy Auto-fallback fires; daemon picks 8081 / 8766 The success panel and halton-meter status show the actual port with (fallback from busy default :8080/:8765). Configure your tool to target the actual port.
Cert is already trusted Idempotent path Init skips the trust step and proceeds. Re-runnable safely.
halton-meter status says BROKEN Some component drifted Run halton-meter doctor for a row-by-row diagnosis with concrete next-actions. Each row has a copy-paste fix.
Live daemon already running, plist needs refresh halton-meter init will prompt before unloading Confirm with y at the prompt to proceed (briefly drops the live API connection during the restart) or n to skip the supervisor refresh.

Switching between modes is a single command — v0.1.5 reconciles all mode-specific state in either direction (no uninstall step required):

halton-meter init                  # env-only (default)
halton-meter init --apps           # apps (env vars, no system proxy)
halton-meter init --full           # full (env vars + system proxy)

The reconciler covers all 9 cells of the (env-only, apps, full) -> (env-only, apps, full) transition matrix. Any stale state from a prior mode (system proxy, launchctl env, shell-rc block, proxy-managed sentinel) is cleaned up to match the requested mode at the end of each init run.

Full uninstall (preserves db.sqlite by default):

halton-meter uninstall                          # remove plists, restore proxy
halton-meter uninstall --purge                  # also delete config + sentinels
halton-meter uninstall --purge --include-logs   # also delete db.sqlite

Three install modes

Halton Meter ships with three install modes, picked at init time:

Mode Invoke What it sets up What gets metered Risk
env-var-only (default) halton-meter init CA cert + daemon + launchd plists only. Does not touch system proxy, launchctl env, or shell rc. Whatever you explicitly route via halton-meter run <command> (Claude Code, the Anthropic SDK, python my_script.py, an interactive subshell with --shell, …). Cannot break HSTS browsing — we never touch the system proxy.
apps (NEW in v0.1.5) halton-meter init --apps Above plus writes HTTPS_PROXY / NODE_EXTRA_CA_CERTS / friends into launchd's user domain (so Spotlight/Dock-spawned apps see them) plus appends an idempotent block to your shell rc (so new terminals inherit them). Does not enable the system proxy. Spotlight/Dock-spawned apps that respect HTTPS_PROXY env (Claude Desktop, JetBrains IDEs, …) and new shells launched after the install. Not browsers. Cannot break HSTS browsing — system proxy is still untouched.
full halton-meter init --full All of apps plus enables the macOS system proxy. Browsers, GUI apps that ignore HTTPS_PROXY env, every other client. May break HSTS browser sites if the CA isn't trusted by SecTrust. The daemon refuses to enable the proxy when cert verification fails (P0-1 in v0.1.3).

--gui is preserved as a deprecated alias for --full (legacy v0.1.3/v0.1.4 sentinel value gui is auto-migrated to full on next init run).

Decision tree

  • "I just want to meter my Claude Code / SDK calls and not worry about anything else." -> env-only (default).
  • "I want every app I open from Spotlight to be metered, but I don't want my browser to break if anything goes wrong with the cert." -> --apps.
  • "I want to capture every HTTPS request my Mac makes, including browser traffic, and I'm OK with the cert-trust step being a hard prerequisite." -> --full.

Witnessed-failure rationale (why three modes, not two)

v0.1.3 shipped just two — env-only and --gui — driven by two witnessed regressions on Vikrant's MacBook 2026-04-30: (1) Claude Code (Node.js) bypasses the macOS system proxy entirely, so a system-proxy- only install captured zero traffic; (2) Chrome / SecTrust rejected the proxied connection on totaljobs.com (HSTS) when the cert was in a mid-trust state, breaking browsing. The --apps mode added in v0.1.5 captures the middle ground: terminal + Spotlight-app coverage WITHOUT the system-proxy-can-break-browsing risk. Specifically, --apps covers the Windsurf IDE / Claude Desktop / Cursor / JetBrains case (Spotlight-spawned, Node-bypasses-system-proxy clients) without touching the system proxy panel.

Architecture note (v0.1.6 — edge-proxy decoupling)

From v0.1.6 the install lays down three macOS LaunchAgents (was two): the daemon, the watchdog, and a new always-on edge proxy.

  • com.haltonlabs.meter.edge — tiny stdlib-only TCP CONNECT proxy. Owns the user-facing port (default :8081). Apps see this in HTTPS_PROXY. Always running (KeepAlive=true). When the metering daemon is healthy, it chains CONNECTs through to the daemon's internal port for full metering. When the daemon is down (stopped, crashing, upgrading, etc.), it tunnels traffic directly to upstream — no metering, but no ConnectionRefused for your IDE / terminal / Claude Code session either.
  • com.haltonlabs.meter — the daemon (mitmproxy + FastAPI + SQLite). Listens on internal_port (default :8090), loopback-only, never advertised in any env var.
  • com.haltonlabs.meter.watchdog — unchanged from v0.1.5.

What this means in practice: halton-meter stop no longer breaks your running apps. They keep working in passthrough until you halton-meter start again. halton-meter uninstall is the only operation that actually unbinds the user-facing port; running apps need to be restarted after that, and the uninstall output says so explicitly.

The edge's daemon-health probe is cached with asymmetric TTLs: edge_health_ttl_ms (default 1000) for the healthy state, fixed 200ms for the unhealthy state. A ConnectionRefusedError on a real chain attempt invalidates the cache instantly so the next request re-probes — no TTL wait. To tune the healthy TTL add [daemon] edge_health_ttl_ms = 500 (or whatever) to ~/.halton-meter/config.toml. The unhealthy TTL is not user-tunable.

If you pinned [daemon] listen_port in config.toml from v0.1.5, v0.1.6 honours it as edge_port and logs a one-shot deprecation warning. Rename the key to edge_port at your convenience; the legacy alias is preserved through v0.1.7.

Useful commands

halton-meter init [--apps|--full] [--no-shell-rc]  # install (re-runnable; idempotent)
halton-meter run <cmd> [args...]             # exec wrapper that injects metering env
halton-meter run --shell                     # interactive subshell with metering env
halton-meter status [--json]                 # mode-aware HEALTHY / INCONSISTENT / BROKEN
halton-meter doctor [--json] [--curl]        # one-command diagnostic; copy into a ticket
halton-meter start | stop                    # supervisor control
halton-meter uninstall [--purge] [--include-logs]
halton-meter reset-proxy                     # emergency: disable system proxy + reset

halton-meter doctor is the diagnostic of last resort: it prints every signal that matters (daemon health, cert trust, system proxy state, launchctl env, shell rc marker, env in current process) and ends with a top-line verdict and concrete next-actions. --curl adds an end-to-end TLS smoke test against https://example.com (hard-coded — never a provider domain).

What it does

  • Intercepts HTTPS traffic to LLM provider endpoints via a local mitmproxy instance
  • Parses request/response bodies to extract model, tokens, cost
  • Attributes each request to a project based on the calling process
  • Stores everything locally in SQLite (~/.halton-meter/db.sqlite)
  • Optionally syncs to a backend for dashboard visualisation

What it doesn't do

  • Doesn't wrap SDKs — your code stays exactly as it is
  • Doesn't intercept anything you don't ask it to — only configured LLM endpoints
  • Doesn't send your data anywhere by default — runs entirely locally
  • Doesn't break your workflow — if the proxy fails, traffic falls through to the real provider

Known limitations

Honest list, not a roadmap. Apps that ignore both the macOS system proxy AND HTTPS_PROXY env have no general capture path:

  • Go binaries with GODEBUG=netdns=go. Go's pure-Go resolver path bypasses HTTPS_PROXY env in some builds. Calls reach the provider unmetered. Mitigation: launch the binary via halton-meter run (sets HTTPS_PROXY explicitly) — but if the binary opts out of proxy env, even that doesn't help.
  • libcurl callers using --insecure or with CURLOPT_PROXY not honoured. Same root cause: the client doesn't consult either signal. halton-meter run will set HTTPS_PROXY, but --insecure callers skip TLS verification entirely.
  • WSL2 networking on Windows hosts. v1.0 doesn't route WSL2 guest traffic through the Windows-side proxy. WSL2 + halton-meter is not yet supported. Linux-native and macOS are the supported platforms.
  • Hardcoded HTTP stacks that open raw TLS sockets to known provider IPs. Anything that bypasses both system proxy AND HTTPS_PROXY env will reach the provider unmetered.
  • Claude Code (Node.js) requires HTTPS_PROXY env. Node honours HTTPS_PROXY, not the macOS system proxy panel. In --gui mode the launchctl user-domain setenv covers Spotlight/Dock-spawned apps; for terminal-launched Claude Code, use halton-meter run claude (env-only mode) or open a new shell after the install (so the rc-appended export takes effect).
  • Late-coming network interfaces. macOS network services are enumerated once when the daemon starts (cached for the process lifetime). If you plug in a new interface — Thunderbolt dock, USB-tethered iPhone — restart with halton-meter stop && halton-meter start.

Project

Halton Meter is open source under Apache 2.0. Built by Halton Labs.

This package is the daemon component. The dashboard lives in the same repository and is run separately.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

halton_meter-0.1.9.tar.gz (293.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

halton_meter-0.1.9-py3-none-any.whl (203.8 kB view details)

Uploaded Python 3

File details

Details for the file halton_meter-0.1.9.tar.gz.

File metadata

  • Download URL: halton_meter-0.1.9.tar.gz
  • Upload date:
  • Size: 293.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.15

File hashes

Hashes for halton_meter-0.1.9.tar.gz
Algorithm Hash digest
SHA256 0f1ed6b61d566cffee4b0db43798c82f3faf44abc710e7a0dbf391556c942488
MD5 f0a848680b0eed775c4172ab1fe9fa34
BLAKE2b-256 997d1903238614a5a295597676eebdadc0774ec3ddf302ad480f0c7a1b2dda3e

See more details on using hashes here.

File details

Details for the file halton_meter-0.1.9-py3-none-any.whl.

File metadata

File hashes

Hashes for halton_meter-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 522f5ee15fdf80e1e56c3df45d39ee5520908ae768d8875bac7eb3137c2edc9d
MD5 4a23bc2cf186876c8e5c2f8154775464
BLAKE2b-256 20360e8088e6c0c74aee50d4334b78a0ffaf34f76516a514a91fe265a0cf0061

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page