Files
cleveragents-core/.opencode/telemetry
drew 62d4e8f07d fix(telemetry): cost dashboard now computes real USD totals
Three intertwined bugs caused every Cost-tab row to display \$0
even after the scraper started writing real token data:

1. **Lookup key mismatch.** ``_cost_usd`` looked up bare ``model``
   but ``_DEFAULT_PRICES`` was keyed by ``provider/model`` — every
   priced model silently missed. Fixed by adding ``_price_key`` and
   a fallback chain: ``provider/model`` → bare ``model`` → ``_unknown``.

2. **SQL grouped by model only.** Same modelID served by two providers
   (e.g. ``claude-opus-4-6`` via Anthropic direct vs a local proxy) at
   different rates was conflated into one row. Fixed: ``GROUP BY model,
   provider`` in ``_api_cost`` + ``provider`` returned in each row.

3. **Math convention mismatch.** ``_cost_usd`` did ``(tokens_in -
   cached) * in_rate`` assuming ``tokens_in`` was total input. But
   the scraper records ``tokens_in`` as OpenCode's ``info.tokens.input``
   (fresh, non-cached), so ``tokens_in - cached`` went negative
   whenever cache reads exceeded fresh input — which is the common
   case with Anthropic prompt caching. Fixed: no subtraction; the
   three populations bill at their three rates.

Pricing seeded for the 8 models the scraper has actually observed
(``_DEFAULT_PRICES`` corrected from stale Opus-3 numbers + new entries
for the Haiku 4.5 / GPT-5 family / CleverThis HF endpoints):

| Provider     | Model                     | in    | out   | cached_in |
|--------------|---------------------------|-------|-------|-----------|
| local-claude | claude-opus-4-6           | 5.00  | 25.00 | 0.50      |
| local-claude | claude-sonnet-4-6         | 3.00  | 15.00 | 0.30      |
| local-claude | claude-haiku-4-5          | 1.00  | 5.00  | 0.10      |
| openai       | gpt-5 / gpt-5-codex       | 1.25  | 10.00 | 0.125     |
| openai       | gpt-5-mini                | 0.25  | 2.00  | 0.025     |
| openai       | gpt-5-nano                | 0.05  | 0.40  | 0.005     |
| CleverThis-* | (HF endpoints, advisory)  | 0.50  | 1.00  | —         |

Operators can override without touching code via
``.opencode/telemetry/prices.json`` (added; same keying convention).
``_load_prices`` now skips ``_comment`` / ``_last_updated`` /
``_sources`` metadata keys so docs in the JSON don't pollute the table.

Smoke against current ``llm_activity`` (3743 turns, 450 archives):
total USD over the lifetime window is now \$118.06, with all 8 models
showing as priced.

Tests pin all three regressions: provider-qualified lookup, bare-model
fallback, no-subtraction math, GROUP BY (model, provider), and the
metadata-key filter in ``_load_prices``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:47:54 -04:00
..

Pipeline Telemetry Console

A small, dependency-free HTTP console that consolidates everything you need to monitor the auto-agents pipeline into a single browser tab. Stdlib-only Python — no pip install required.

Why

Pipeline state lives in five silos today: the SQLite cache (merge_cycle / conflict_drive_cycles / llm_activity / pulls / commits), the running daemon processes, per-driver heartbeat files, the OpenCode HTTP server, and the live Forgejo API. Tailing five terminals + ad-hoc sqlite3 + curl is fine for the operator who built the pipeline; it does not scale to "another dev wants to see what's happening". This console is the consolidated view.

Run

# Always source the launcher first so the console reads the right
# cache and hits the right Forgejo target.
source tools/launch_fork.sh

# Default: bind 127.0.0.1:8765
python3 .opencode/telemetry/server.py

# Custom host/port
python3 .opencode/telemetry/server.py --host 127.0.0.1 --port 8765
# or via env
TELEMETRY_HOST=127.0.0.1 TELEMETRY_PORT=8765 \
  python3 .opencode/telemetry/server.py

# Browse:
#   http://127.0.0.1:8765/

The console never writes to Forgejo, OpenCode, or the SQLite cache. It is read-only by design.

Tabs

Tab What it shows Refresh Source
Overview Daemon up/down + heartbeat freshness + last 5 cycles per driver 5 s /proc walk · file mtime · cache
Drivers Full merge_cycle and conflict_drive_cycles tables (last 50) + 24 h outcome breakdown 15 s cache
PRs Open PRs by auto/* label · live Forgejo query · click-through to web UI 30 s Forgejo API
Velocity Window stats (24 h / 48 h / 7 d / 30 d) · merged / opened / commits 5 min cache
LLM Active OpenCode sessions · message counts · created/updated ages 10 s OpenCode API
Cost Token usage and estimated cost by model · selectable window 60 s llm_activity table + prices.json
About Environment audit · data-source map · operator notes static /api/meta

Inactive tabs don't poll — switching tabs cancels the previous tab's interval. Server load on a long-lived browser session stays flat.

Connection-status indicator

The pulsing dot in the top-left turns:

  • Blue (pulsing) — fetching, normal state
  • Green — last fetch succeeded
  • Red (fast pulse) — last fetch failed (API unreachable, JSON parse error, server returned non-2xx). Hover to see when it last succeeded; click any tab to retry.

Repo target

The header banner shows <owner>/<repo> and a cache OK / cache MISSING indicator. If you sourced tools/launch_fork.sh, expect to see drew/cleveragents-core. If you forgot to source it, you'll see cleveragents/cleveragents-core (canonical) — restart the console after sourcing the right shell to switch.

Cost tracking — instrumentation status

The Cost tab reads aggregated token usage from the llm_activity(model, tokens_in, tokens_out, cached_tokens) table. The table exists with the right schema; the drivers do not yet write to it. The tab will display an Instrumentation pending notice while this is the case, and populate automatically once the writers land.

The token → USD conversion uses a per-model price table in .opencode/telemetry/prices.json (override) or the seeded defaults in server.py (_DEFAULT_PRICES). Prices are listed as dollars per million tokens for in / out / cached_in. To override:

// .opencode/telemetry/prices.json
{
  "anthropic/claude-opus-4-6": { "in": 15.0, "out": 75.0, "cached_in": 1.5 },
  "CleverThis-15/Qwen3-6-35B-A3B-GGUF-UD-Q3-K-XL": { "in": 0.5, "out": 1.0 }
}

Models without a price entry are surfaced with an unpriced pill so operators know to add an entry rather than treat the row as $0.

Security posture

  • Default bind: 127.0.0.1 (loopback only). No remote-by-default.
  • No authentication in v1. The loopback bind is the security model.
  • The console does not expose secretsGITEA_TOKEN, FORGEJO_PAT, etc. are read into the server process and used to authenticate outbound API calls; their values never appear in any endpoint response. The /api/meta endpoint includes a boolean has_forgejo_token so you can confirm presence without leaking the value.
  • Exposing on a public interface requires a reverse proxy with auth in front of it. Setting TELEMETRY_HOST=0.0.0.0 is a deliberate decision; the server logs the bind interface at startup so this is visible.

Architecture

.opencode/telemetry/
├── server.py     # stdlib HTTPServer, all /api/* + static
├── index.html    # tabbed SPA, no build step
├── app.js        # vanilla JS, per-tab fetch loops
├── style.css     # minimal, prefers-color-scheme aware
├── prices.json   # (optional) token-cost overrides
└── README.md     # this file

Endpoints under /api/* are documented in server.py's module docstring. Each is a thin SQLite query, urllib.request call, or /proc walk — no business logic, easy to extend.

Add a new metric / tab

  1. Add a query function in server.py next to the existing _api_* helpers.
  2. Wire it into _API_DISPATCH with a unique /api/... path.
  3. Add a <section class="pane" data-pane="<name>"> to index.html and a <button class="tab" data-tab="<name>"> in the nav.
  4. Add a PANES['<name>'] = { intervalMs, refresh } entry in app.js.

The whole thing fits in four files; you can read it end-to-end in under ten minutes. By design.

Limitations / planned

  • No charts in v1. Window stats are shown as numbers; the canvas rendered by tools/reports.sh keeps the historical-trend story (weekly throughput, monthly volume, daily series). The console focuses on live/short-window pipeline state, not historical analytics. If we want charts here later, a single <canvas> + vanilla JS plot is the right way to add them — staying dependency-free.
  • No SSE / WebSocket. v1 is plain polling. SSE would cut the no-data-changed re-render cost; not worth the complexity until observed.
  • No request log persistence. Server only logs warnings; if you want a full request log, run with --log-level DEBUG.
  • HEAD is not implemented. Returns 501. Browsers and the SPA only use GET so this hasn't bitten anyone; trivial to add when needed.

Troubleshooting

Symptom Fix
Header shows cleveragents/cleveragents-core but you wanted the fork You forgot source tools/launch_fork.sh. Stop the console, source it, restart.
cache MISSING pill in header The cache file has not been created yet for this (owner, repo). Run any cache-syncing tool (e.g. python3 tools/sync-forgejo-cache.py) once.
Cost tab shows unpriced for the model your driver actually uses Add an entry to prices.json (or the _DEFAULT_PRICES dict in server.py).
LLM tab is empty even though OpenCode is running The console queries GET /session against OPENCODE_URL (default http://127.0.0.1:4096). Set OPENCODE_URL if the server is on a different port.
PRs tab says forgejo unreachable or no token FORGEJO_PAT (or GITEA_TOKEN) is empty in the console's environment. Source launch_fork.sh and restart.