ModelTap

Know exactly how many tokens and how much money your AI coding agents use.

ModelTap is a transparent, network-level observability proxy for Codex, Claude Code, Gemini CLI, oh-my-pi, and other AI clients. It measures the API traffic that actually reaches model providers and exports token usage and estimated cost to Grafana or any OTLP/HTTP-compatible backend.

No SDK. No agent patches. No vendor-specific telemetry integration.

ModelTap Grafana dashboard showing QPS, token usage, estimated cost, and agent breakdowns

ModelTap recognizes OpenAI, Anthropic, Gemini, and DeepSeek API usage; detects common agent CLIs automatically; supports streaming SSE and WebSocket traffic; and can forward directly or through HTTP, HTTPS, and SOCKS5 egress proxies.

Why ModelTap?

Native agent telemetry and log parsers can be useful, but neither provides the same provider-independent view of what left the machine. ModelTap observes the model API boundary instead.

ModelTapNative agent telemetryLog-file parsers
One view across agent CLIsYesUsually agent-specificVaries
Measures API usage at the network boundaryYesDepends on the agentNo
Requires an agent plugin or code changeNoOftenNo
Exports standard OTLP metricsYesVariesVaries
Works with a configured egress proxyYesDepends on the agentN/A

What you get

Architecture

ModelTap request path

AI client / CLI
  │  HTTP_PROXY, HTTPS_PROXY, or PI_PROXY
  ▼
ModelTap explicit proxy
  ├─ CONNECT host is absent from sites ──► transparent tunnel ──► upstream API
  └─ CONNECT host matches a site
       │
       ├─ TLS MITM and HTTP/1.1 or HTTP/2 forwarding ──────────► upstream API
       ├─ SSE / WebSocket / Cursor Connect side parser
       │    └─ detects protocol and extracts model and token usage
       └─ UsageObserver
            ├─ PriceBook: site + model + current pricing window
            ├─ structured usage log
            └─ Telemetry counters and latency histograms

sites is the inspection allowlist. A matching host is always decrypted with the local CA; unmatched hosts are forwarded transparently and do not produce usage data. Protocol detection is internal to parsing and is not a configuration field or a metric label. The proxy forwards request and response streams without waiting for full bodies; parsing observes copies of SSE events, WebSocket text frames, and Cursor Connect messages.

Usage and cost reporting path

UsageObserver
  │  tokens + configured price rule
  ▼
OpenTelemetry metrics in ModelTap
  │  OTLP/HTTP POST to <telemetry.otlp.endpoint>/v1/metrics
  ▼
Grafana Alloy OTLP receiver
  │  Prometheus remote_write
  ▼
Grafana Cloud Prometheus ──► Explore and ModelTap dashboard

ModelTap emits the cumulative counters ai_proxy_requests, ai_proxy_tokens, and ai_proxy_cost, plus proxy and telemetry latency histograms. Usage counter labels are site, model, agent_cli, token type, price_period, and currency where applicable. The client response path does not wait for remote write: the OTLP SDK batches exports in the background. In Grafana, select data in the order Agent CLI → Site → Model.

Quick start

Install ModelTap and create the local root CA once. Keep the private key secret and install the generated certificate in your operating system or client trust store:

macOS / Linux

# 1. Install via Homebrew (automatically generates config.yaml and CA certificates in $(brew --prefix)/etc/modeltap/)
brew install tenfyzhong/tap/modeltap

# 2. Trust the root CA certificate in your OS trust store
# macOS:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain \
  "$(brew --prefix)/etc/modeltap/certs/ca-cert.pem"
# Linux:
sudo cp "$(brew --prefix)/etc/modeltap/certs/ca-cert.pem" /usr/local/share/ca-certificates/modeltap-ca-cert.crt
sudo update-ca-certificates

# 3. Validate configuration
modeltap validate --config "$(brew --prefix)/etc/modeltap/config.yaml"

# 4. Run ModelTap (interactive or background service)
modeltap run --config "$(brew --prefix)/etc/modeltap/config.yaml"
# or start in background:
brew services start tenfyzhong/tap/modeltap

Windows (PowerShell)

# 1. Install from source or download the prebuilt binary from Releases
cargo install --path .

# 2. Initialize local root CA
New-Item -ItemType Directory -Force -Path certs
modeltap ca-init `
  --cert certs\modeltap-ca-cert.pem `
  --key certs\modeltap-ca-key.pem

# 3. Trust the root CA certificate in Windows Certificate Store
Import-Certificate -FilePath .\certs\modeltap-ca-cert.pem -CertStoreLocation Cert:\CurrentUser\Root

# 4. Copy sample configuration and validate
Copy-Item config.sample.yaml config.yaml
modeltap validate --config config.yaml

# 5. Run ModelTap
modeltap run --config config.yaml

Before starting the proxy, set the telemetry endpoint, egress route, and pricing rules for your environment (edit $(brew --prefix)/etc/modeltap/config.yaml on macOS/Linux or config.yaml on Windows). The bundled configuration uses a sample privoxy egress route; set egress.default: direct if you do not use an upstream proxy. Use modeltap validate --config <CONFIG> to check YAML syntax, site and egress validation, and pricing rules without binding a listener or reading certificate files. Shell completions are included in completions/ (and installed automatically by Homebrew on macOS/Linux); source modeltap.bash, _modeltap, modeltap.fish, or modeltap.ps1 for Bash, Zsh, Fish, or PowerShell.

Set your browser, CLI, or SDK HTTP proxy to http://127.0.0.1:2080. Every host configured under sites is decrypted; other CONNECT tunnels remain transparent.

Each hosts entry is a domain root and includes all subdomains at a DNS label boundary. For example, googleapis.com includes generativelanguage.googleapis.com, but does not match notgoogleapis.com. Overlapping domain trees across sites are rejected.

The inbound proxy does not require client authentication, including on a non-loopback listener such as 0.0.0.0:2080. Protect non-loopback listeners with a host firewall, private network, or another trusted access-control layer to avoid running an open proxy.

Install and trust the local CA

Install the generated root CA in your operating system trust store so that client applications and CLI tools trust the TLS MITM certificate:

Client and agent configuration

Node.js uses its own CA bundle and does not trust locally installed root CAs by default. Point NODE_EXTRA_CA_CERTS at the absolute path of the ModelTap CA, then restart your agent or background daemon:

macOS / Linux (Bash / Zsh)

export NODE_EXTRA_CA_CERTS="$(brew --prefix)/etc/modeltap/certs/ca-cert.pem"
export HTTP_PROXY=http://127.0.0.1:2080
export HTTPS_PROXY=http://127.0.0.1:2080
export PI_PROXY=http://127.0.0.1:2080
omp

macOS / Linux (Fish)

set -x NODE_EXTRA_CA_CERTS (brew --prefix)/etc/modeltap/certs/ca-cert.pem
set -x HTTP_PROXY http://127.0.0.1:2080
set -x HTTPS_PROXY http://127.0.0.1:2080
set -x PI_PROXY http://127.0.0.1:2080
omp

Windows (PowerShell)

$env:NODE_EXTRA_CA_CERTS = "$pwd\certs\modeltap-ca-cert.pem"
$env:HTTP_PROXY = "http://127.0.0.1:2080"
$env:HTTPS_PROXY = "http://127.0.0.1:2080"
$env:PI_PROXY = "http://127.0.0.1:2080"
omp

Windows (Command Prompt)

set NODE_EXTRA_CA_CERTS=%cd%\certs\modeltap-ca-cert.pem
set HTTP_PROXY=http://127.0.0.1:2080
set HTTPS_PROXY=http://127.0.0.1:2080
set PI_PROXY=http://127.0.0.1:2080
omp

PI_PROXY routes all oh-my-pi providers through ModelTap. A provider-specific variable overrides it; use PI_PROXY_CURSOR when Cursor needs another proxy endpoint. oh-my-pi uses a dedicated HTTP/2 transport for Cursor Agent traffic, so PI_PROXY or PI_PROXY_CURSOR is required for Cursor models, including Grok, to reach ModelTap.

Python & other CLI tools

Running as a background service

To run ModelTap persistently in the background on startup, configure it as an operating system service:

macOS & Linux (Homebrew services)

If installed via Homebrew, manage ModelTap directly with brew services:

# Start ModelTap service in background
brew services start tenfyzhong/tap/modeltap

# Check service status
brew services info tenfyzhong/tap/modeltap

# Restart or stop the service
brew services restart tenfyzhong/tap/modeltap
brew services stop tenfyzhong/tap/modeltap

Linux (systemd)

For standalone Linux installations without Homebrew, create /etc/systemd/system/modeltap.service:

[Unit]
Description=ModelTap AI Traffic Monitor
After=network.target

[Service]
Type=simple
User=modeltap
WorkingDirectory=/opt/modeltap
ExecStart=/opt/modeltap/modeltap run --config /opt/modeltap/config.yaml
Restart=always
RestartSec=5
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now modeltap

Windows Service (NSSM)

NSSM (Non-Sucking Service Manager) runs ModelTap as a Windows service with automatic restart on crash:

# 1. Install NSSM (Run PowerShell as Administrator)
winget install NSSM.NSSM

# 2. Install and configure ModelTap service
nssm install ModelTap "C:\modeltap\modeltap.exe" "run --config C:\modeltap\config.yaml"
nssm set ModelTap AppDirectory "C:\modeltap"
nssm set ModelTap Start SERVICE_AUTO_START
nssm set ModelTap AppStdout "C:\modeltap\logs\service-stdout.log"
nssm set ModelTap AppStderr "C:\modeltap\logs\service-stderr.log"

# 3. Start the service
nssm start ModelTap

# 4. Service management
nssm status ModelTap
nssm restart ModelTap
nssm stop ModelTap

Windows Service (WinSW)

Alternatively, download WinSW (e.g. WinSW-x64.exe), place it as modeltap-service.exe next to modeltap.exe, and create modeltap-service.xml:

<service>
  <id>ModelTap</id>
  <name>ModelTap AI Proxy</name>
  <description>ModelTap AI traffic monitor and cost proxy</description>
  <executable>C:\modeltap\modeltap.exe</executable>
  <arguments>run --config C:\modeltap\config.yaml</arguments>
  <workingdirectory>C:\modeltap</workingdirectory>
  <logpath>C:\modeltap\logs</logpath>
  <log mode="roll-by-size">
    <sizeThreshold>10240</sizeThreshold>
    <keepFiles>5</keepFiles>
  </log>
</service>

Install and start the service:

.\modeltap-service.exe install
.\modeltap-service.exe start

Configuration

Copy config.sample.yaml to config.yaml before first use and adjust its certificate paths, telemetry endpoint, egress proxy, sites, and pricing rules. The sample is a complete local configuration. OpenAI, Anthropic, and Gemini use Privoxy at 127.0.0.1:8118; DeepSeek explicitly uses the direct egress and never uses Privoxy.

proxy:
  listen: 127.0.0.1:2080

logging:
  level: info
  # file: ./logs/modeltap.log

tls:
  ca_cert_file: ./certs/modeltap-ca-cert.pem
  ca_key_file: ./certs/modeltap-ca-key.pem

telemetry:
  otlp:
    endpoint: http://127.0.0.1:4318
    service_name: modeltap-test

egress:
  default: privoxy
  proxies:
    - id: privoxy
      url: http://127.0.0.1:8118

sites:
  - id: openai
    hosts:
      - chatgpt.com
  - id: anthropic
    hosts:
      - anthropic.com
  - id: gemini
    hosts:
      - googleapis.com
  - id: deepseek
    hosts:
      - api.deepseek.com
    egress: direct
  - id: grok
    hosts:
      - api.x.ai
  - id: cursor
    hosts:
      - cursor.sh
      - cursor.com

pricing:
  timezone: Asia/Shanghai
  # Official first-party API list prices checked on 2026-08-20.
  # All rates are USD per 1M tokens. DeepSeek CNY prices were converted with
  # the 2026-08-19 ECB rates (1 CNY = 0.148407227899 USD).
  peak_windows:
    - weekdays: [1, 2, 3, 4, 5]
      start: "09:00"
      end: "12:00"
    - weekdays: [1, 2, 3, 4, 5]
      start: "14:00"
      end: "18:00"
  rules:
    - model: "gpt-5.6-sol*"
      currency: USD
      rates:
        input: 5
        output: 30
        cache_read: 0.5
    - model: "gpt-5.6-terra*"
      currency: USD
      rates:
        input: 2
        output: 12
        cache_read: 0.2
    - model: "gpt-5.6-luna*"
      currency: USD
      rates:
        input: 0.2
        output: 1.2
        cache_read: 0.02
    - model: "claude-opus-4-8*"
      currency: USD
      rates:
        input: 5
        output: 25
        cache_read: 0.5
        cache_write: 6.25
    - model: "claude-sonnet-4-6*"
      currency: USD
      rates:
        input: 3
        output: 15
        cache_read: 0.3
        cache_write: 3.75
    - model: "claude-haiku-4-5*"
      currency: USD
      rates:
        input: 1
        output: 5
        cache_read: 0.1
        cache_write: 1.25
    - model: "gemini-3.7-flash*"
      currency: USD
      rates:
        input: 0.75
        output: 3.75
        cache_read: 0.075
    - model: "deepseek-v4-flash*"
      currency: USD
      peak_windows:
        - weekdays: [1, 2, 3, 4, 5]
          start: "09:00"
          end: "12:00"
        - weekdays: [1, 2, 3, 4, 5]
          start: "14:00"
          end: "18:00"
      peak:
        input: 0.445221684
        output: 1.335665051
        cache_read: 0.014840723
      off_peak:
        input: 0.222610842
        output: 0.667832526
        cache_read: 0.007420361
    - model: "deepseek-v4-pro*"
      currency: USD
      peak_windows:
        - weekdays: [1, 2, 3, 4, 5]
          start: "09:00"
          end: "12:00"
        - weekdays: [1, 2, 3, 4, 5]
          start: "14:00"
          end: "18:00"
      peak:
        input: 1.335665051
        output: 4.006995153
        cache_read: 0.044522168
      off_peak:
        input: 0.667832526
        output: 2.003497577
        cache_read: 0.022261084
    # Cursor site-specific overrides
    - site: cursor
      model: "gpt-5.6-sol-*"
      currency: USD
      rates:
        input: 2.5
        output: 15
        cache_read: 0.25
    - site: cursor
      model: "gpt-5.6-terra-*"
      currency: USD
      rates:
        input: 1.25
        output: 7.5
        cache_read: 0.125
    - site: cursor
      model: "gpt-5.6-luna-*"
      currency: USD
      rates:
        input: 0.5
        output: 3
        cache_read: 0.05

With this configuration, Grok uses the OpenAI-compatible API at api.x.ai and Cursor uses cursor.sh and cursor.com. Rates are per million tokens. Pricing rules can be configured globally (without a site) or for a specific site as an override. Peak windows can be configured globally or customized per model with an optional weekdays array (e.g. weekdays: [1, 2, 3, 4, 5] or weekdays: ["Mon", "Tue", "Wed", "Thu", "Fri"] where 1 / "Mon" / "Monday" = Monday to 7 / "Sun" / "Sunday" = Sunday), may cross midnight, and must not overlap on the same day. A rates rule applies the same price all day and can coexist with global peak_windows used by another model:

rates:
  input: 0.02
  output: 0

Sites and protocol detection

A site id is the service identity used in metric labels and pricing.rules; use the actual vendor or service name, such as grok, cursor, or openai. ModelTap detects the usage protocol from each request or response automatically. Site configuration therefore has no provider or provider_type field. It recognizes Cursor Connect/Protobuf, Gemini usage metadata, Anthropic message events, and OpenAI Chat/Responses payloads. A DeepSeek site can report both OpenAI-compatible and Anthropic-compatible traffic without any special setting.

Every host configured under sites is intercepted with TLS MITM. This makes sites the explicit allowlist of traffic that ModelTap can inspect. Hosts absent from sites are forwarded without MITM and no usage is collected. Remove the former provider, provider_type, and mitm fields when migrating an existing configuration.

Cursor Agent traffic uses Connect/Protobuf. ModelTap reads the selected model ID from each request, so Cursor models such as GPT, Claude, Grok, GLM, Gemini, and Composer are reported without a model allowlist. Cursor reports generated-token increments; configure pricing.rules for site: cursor when you want costs. The agent_cli metric label is inferred from stable built-in client-header rules: claude_code, codex, gemini_cli, oh_my_pi, opencode, pi, github_copilot, amazon_q, roo_code, qwen_code, factory_droid, crush, kiro, qoder, antigravity, cursor, or unknown. Tools without a distinctive request header, including Aider, Goose, and Continue, remain unknown rather than risking an incorrect classification. Raw User-Agent values are never metric labels, avoiding high-cardinality metrics.

Logging

Supported levels are error, warn, info, debug, and trace. At info, every parsed usage report logs its site, model, token totals, pricing period, and calculated cost. Set logging.level: debug to also log routing decisions, response status, SSE detection, WebSocket frame bytes, and bounded request and response body previews.

Set logging.file to append the same logs to a file while retaining stderr output. ModelTap creates the file but not its parent directory.

Debug previews are limited to 4 KiB per chunk and authentication headers are never logged, but prompt and model-response content can still appear in logs. Enable debug logging only in a trusted environment.

Grafana Alloy

Alloy receives ModelTap metrics over OTLP/HTTP and forwards them to Grafana Cloud with Prometheus remote write. ModelTap posts to <telemetry.otlp.endpoint>/v1/metrics.

1. Install Alloy

On macOS, install and start the Homebrew service:

brew install grafana/grafana/alloy
brew services start grafana/grafana/alloy
brew services info grafana/grafana/alloy

On Debian or Ubuntu, install Alloy from Grafana's official APT repository and enable its systemd service:

sudo apt-get install -y gpg wget
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
sudo chmod 644 /etc/apt/keyrings/grafana.asc
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install alloy
sudo systemctl enable --now alloy

For RHEL, Fedora, SUSE, Windows, Docker, or Kubernetes, follow Grafana's Alloy installation guide.

2. Create Grafana Cloud credentials

  1. Open the Grafana Cloud Portal, navigate to Manage your Grafana Cloud stack, and click Launch to open your Grafana instance.

    Launch Grafana instance in Grafana Cloud Portal
  2. In the Grafana navigation sidebar, navigate to ConnectionsData sources, and search for prometheus. Select your stack's default Prometheus data source (e.g. grafanacloud-*-prom).

    Search Prometheus under Connections -> Data sources
  3. Under Connection, copy the Prometheus server URL to use as AGENT_USAGE_PROMETHEUS_URL in Alloy's config.env. Under Authentication, copy the User (numeric ID) to use as AGENT_USAGE_PROMETHEUS_USERNAME in Alloy's config.env.

    Copy Prometheus server URL and User ID
  4. In the Grafana Cloud Portal left sidebar, click Access Policies (under SECURITY). Create a new policy with write permissions or reuse an existing policy with set:alloy-data-write scope, then click Add token to create a token. Copy the token to use as AGENT_USAGE_PROMETHEUS_PASSWORD in Alloy's config.env.

    Create Access Policy with write permissions and generate a Token
Use the numeric Prometheus User ID, not your Grafana login username. Keep the access policy token out of config.alloy, shell history, source control, and screenshots. Give it an expiration date and rotate it before it expires.

3. Configure the credentials

On macOS, add the values to $(brew --prefix)/etc/alloy/config.env. Homebrew's Alloy service loads this file:

export AGENT_USAGE_PROMETHEUS_URL="https://prometheus-REGION.grafana.net/api/prom/push"
export AGENT_USAGE_PROMETHEUS_USERNAME="YOUR_NUMERIC_INSTANCE_ID"
export AGENT_USAGE_PROMETHEUS_PASSWORD="YOUR_ACCESS_POLICY_TOKEN"

On Debian or Ubuntu, add the same values without export to /etc/default/alloy, which is the systemd service environment file:

AGENT_USAGE_PROMETHEUS_URL="https://prometheus-REGION.grafana.net/api/prom/push"
AGENT_USAGE_PROMETHEUS_USERNAME="YOUR_NUMERIC_INSTANCE_ID"
AGENT_USAGE_PROMETHEUS_PASSWORD="YOUR_ACCESS_POLICY_TOKEN"

4. Configure the metrics pipeline

Replace the contents of $(brew --prefix)/etc/alloy/config.alloy on macOS or /etc/alloy/config.alloy on Linux with:

logging {
  level  = "info"
  format = "logfmt"
}

otelcol.receiver.otlp "agent_usage" {
  http { endpoint = "127.0.0.1:4318" }
  output { metrics = [otelcol.exporter.prometheus.agent_usage.input] }
}

otelcol.exporter.prometheus "agent_usage" {
  add_metric_suffixes = false
  forward_to = [prometheus.remote_write.default.receiver]
}

prometheus.remote_write "default" {
  endpoint {
    url = sys.env("AGENT_USAGE_PROMETHEUS_URL")

    basic_auth {
      username = sys.env("AGENT_USAGE_PROMETHEUS_USERNAME")
      password = sys.env("AGENT_USAGE_PROMETHEUS_PASSWORD")
    }
  }
}

5. Validate and start the pipeline

Validate the configuration before restarting Alloy:

# macOS
alloy validate "$(brew --prefix)/etc/alloy/config.alloy"
brew services restart grafana/grafana/alloy
tail -f "$(brew --prefix)/var/log/alloy.err.log"

# Debian or Ubuntu
sudo alloy validate /etc/alloy/config.alloy
sudo systemctl restart alloy
sudo journalctl -u alloy -f

Open http://127.0.0.1:12345 and confirm that all Alloy components are healthy. Then start ModelTap, send one request through the proxy, and query ai_proxy_requests in Grafana Cloud Explore using the stack's Prometheus data source. Metrics can take a short time to appear because the OTLP SDK batches exports.

For a host installation of ModelTap, keep telemetry.otlp.endpoint: http://127.0.0.1:4318. The included Docker configuration already uses http://host.docker.internal:4318 so a ModelTap container can reach Alloy running on a Docker Desktop host.

Usage metrics are ai_proxy_requests, ai_proxy_tokens, and ai_proxy_cost. Labels are limited to site, model, agent CLI, token type, pricing period, and currency. Local processing-duration metrics include site and agent CLI labels so internal benchmark and test traffic can be excluded.

Telemetry overhead and latency

Four additional OTLP histograms quantify proxy latency and overhead: ai_proxy_upstream_first_response_seconds measures the time from a proxied request reaching ModelTap until upstream response headers arrive; ai_proxy_processing_duration_microseconds measures, in microseconds, the time from a request entering ModelTap until its response leaves it; ai_proxy_local_processing_duration_microseconds records one sample for each HTTP body chunk and server-to-client WebSocket frame that ModelTap parses and uses to record usage. It excludes upstream and OTLP export network time; ai_proxy_telemetry_record_duration_seconds measures local usage-metric recording time. The OTLP SDK batches exports in the background instead of waiting in the user response path.

histogram_quantile(0.95,
  sum by (le) (rate(ai_proxy_telemetry_record_duration_seconds_bucket[5m])))

To monitor ModelTap's p95 per-chunk processing overhead, query:

histogram_quantile(0.95,
  sum by (le, site) (rate(ai_proxy_local_processing_duration_microseconds_bucket[5m])))

Docker

Generate the CA files on the host before starting the container:

mkdir -p certs
./target/debug/modeltap ca-init \
  --cert certs/modeltap-ca-cert.pem \
  --key certs/modeltap-ca-key.pem
docker compose up --build -d
docker compose logs -f modeltap

The included Compose configuration binds the proxy to host port 2080, mounts both CA files read-only, and uses host.docker.internal to access GOST on port 1081 and Alloy on port 4318 from Docker Desktop on macOS. Install the CA certificate in each client trust store; do not install or distribute the private key. In production, use Docker Secrets, Kubernetes Secrets, or a secret manager for the CA key.

Grafana Cloud

ModelTap Grafana dashboard with agent, site, and model filters

After ai_proxy_requests appears in Explore, open Dashboards → New → Import, upload grafana/modeltap-dashboard.json, and select the same Prometheus data source. Click Import to create the ModelTap dashboard. Re-import with the same dashboard UID to overwrite an older copy after the JSON changes.

The dashboard reads cumulative counters so the first exported usage sample is visible immediately. Its variables are ordered Agent CLI, Site, and Model, with each selection filtering the following ones. test_client and benchmark_client are excluded from both the variable and every panel query. The second row shows QPS by model; cumulative token and cost charts group data by agent_cli, site, model, and type. Selected-range totals are visible above the default-collapsed Performance row, which contains local processing-duration charts. Token values abbreviate large values as K, M, or B. Cost is displayed in USD.

Docker Hub and binary releases

Pushing a Git tag, or manually dispatching the workflow for an existing tag, publishes Docker images and creates or updates the matching GitHub Release. Configure repository secrets DOCKERHUB_USERNAME and DOCKERHUB_TOKEN. The workflow builds separate linux/amd64 and linux/arm64 images with -amd64 and -arm64 suffixes, then publishes multi-architecture manifests for the version (without a leading v) and latest.

The GitHub Release also contains the Homebrew bottle and compressed standalone binaries: modeltap-<tag>-aarch64-apple-darwin.tar.gz, modeltap-<tag>-x86_64-unknown-linux-gnu.tar.gz, modeltap-<tag>-aarch64-unknown-linux-gnu.tar.gz, and modeltap-<tag>-x86_64-pc-windows-msvc.zip (also available as .tar.gz). A tag whose version cannot be used as a Docker tag fails before publishing.

The release workflow builds a Homebrew bottle and uploads it to the matching GitHub Release. After that upload succeeds, it opens or updates a Formula bump PR for modeltap in the tenfyzhong/tap tap. Set the HOMEBREW_TAP_TOKEN repository secret to a GitHub token that can create pull requests in tenfyzhong/homebrew-tap. Create Formula/modeltap.rb in that tap before publishing the first tag.

Frequently Asked Questions (FAQ)

Codex fails with invalid peer certificate: BadSignature

Symptom: Running codex (or other Rust-based CLI tools) through ModelTap outputs:

Falling back from WebSockets to HTTPS transport.
stream disconnected before completion: invalid peer certificate: BadSignature

Cause: A stale modeltap local CA root certificate exists in your system or login keychain with a different public/private key pair than the active CA private key configured in ModelTap. codex loads the old root certificate from Keychain, and TLS verification fails because the signature on the dynamically generated leaf certificate was produced by the new private key.

Resolution:

  1. Remove the old certificate from the macOS Keychain:

    security delete-certificate -c "modeltap local CA" ~/Library/Keychains/login.keychain-db 2>/dev/null || true
    sudo security delete-certificate -c "modeltap local CA" /Library/Keychains/System.keychain 2>/dev/null || true
  2. Re-install and trust the active ModelTap root CA certificate:

    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain \
      "$(brew --prefix)/etc/modeltap/certs/ca-cert.pem"
  3. Verify that the serial number and public key match between the Keychain and your CA certificate file:

    security find-certificate -c "modeltap local CA" -p | openssl x509 -noout -serial -pubkey
    openssl x509 -in "$(brew --prefix)/etc/modeltap/certs/ca-cert.pem" -noout -serial -pubkey

Codex fails with invalid peer certificate: UnknownIssuer

Symptom: Running codex outputs:

Falling back from WebSockets to HTTPS transport.
stream disconnected before completion: invalid peer certificate: UnknownIssuer

Cause: The ModelTap CA certificate is present in the keychain file, but lacks root trust policy settings (for example, security add-trusted-cert -d was executed without sudo, preventing macOS from writing the admin trust settings). Tools using rustls-native-certs only load root certificates configured with explicit trust settings.

Resolution:

Security

Use TLS interception only for traffic you are authorized to inspect. It exposes request and response content to the proxy process.

See CONTRIBUTING.md and the MIT License.