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>
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 secrets —
GITEA_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/metaendpoint includes a booleanhas_forgejo_tokenso 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.0is 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
- Add a query function in
server.pynext to the existing_api_*helpers. - Wire it into
_API_DISPATCHwith a unique/api/...path. - Add a
<section class="pane" data-pane="<name>">toindex.htmland a<button class="tab" data-tab="<name>">in the nav. - Add a
PANES['<name>'] = { intervalMs, refresh }entry inapp.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.shkeeps 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. |