feat(telemetry): per-1M-token rate annotations + friendly uninitialized-DB note
The cost table now shows the resolved in/out/cached per-1M-token rates in parens next to each priced model, so an operator can see how a row's cost was derived. And controller-DB query failures distinguish "DB reachable but no schema yet" (the normal pre-first-run state, before the master runs create_all) from a real query error, instead of surfacing a raw SQL "no such table" dump.
This commit is contained in:
@@ -709,6 +709,13 @@ document.getElementById('cost-group-select').addEventListener('change', () => {
|
||||
PANES['cost'].refresh();
|
||||
});
|
||||
|
||||
// Format a $/1M-token rate: up to 3 decimals, trailing zeros stripped
|
||||
// ($5, $0.125, $0.05) so the rates stay readable at a glance.
|
||||
function fmtRate(n) {
|
||||
const s = (Number(n) || 0).toFixed(3).replace(/\.?0+$/, '');
|
||||
return '$' + (s || '0');
|
||||
}
|
||||
|
||||
function renderCost(targetId, data) {
|
||||
const host = document.getElementById(targetId);
|
||||
host.innerHTML = '';
|
||||
@@ -736,6 +743,14 @@ function renderCost(targetId, data) {
|
||||
el('th', { class: 'num' }, 'cost USD'))));
|
||||
const tbody = el('tbody');
|
||||
for (const r of rows) {
|
||||
// Per-1M-token rates in parens so the operator can see how the
|
||||
// row's cost was derived. Shown only for priced models — the
|
||||
// "unpriced" pill already conveys the all-zero case.
|
||||
const priceAnno = (!byPr && r.priced && r.price)
|
||||
? el('span', { class: 'muted small', title: 'per 1M tokens' },
|
||||
` (in ${fmtRate(r.price.in)} · out ${fmtRate(r.price.out)}` +
|
||||
` · cached ${fmtRate(r.price.cached_in)} /M)`)
|
||||
: null;
|
||||
const firstCell = byPr
|
||||
? el('td', {},
|
||||
r.pr_number == null
|
||||
@@ -746,7 +761,7 @@ function renderCost(targetId, data) {
|
||||
: el('td', {},
|
||||
el('code', {}, r.model || '_unknown'),
|
||||
' ',
|
||||
r.priced ? null : pill('unpriced', 'warn'));
|
||||
r.priced ? priceAnno : pill('unpriced', 'warn'));
|
||||
tbody.appendChild(el('tr', {},
|
||||
firstCell,
|
||||
byPr ? el('td', { class: 'muted small' },
|
||||
|
||||
@@ -190,6 +190,18 @@ def _iso(value: Any) -> str | None:
|
||||
return value.isoformat() if hasattr(value, "isoformat") else str(value)
|
||||
|
||||
|
||||
def _controller_db_note(exc: Exception) -> str:
|
||||
"""Friendly note for a controller-DB query failure. A DB that exists
|
||||
but has no schema yet (the controller master runs create_all on
|
||||
boot) is the common pre-first-run state — distinguish it from a real
|
||||
query error so the operator isn't shown a raw SQL dump."""
|
||||
msg = str(exc)
|
||||
if any(s in msg for s in ("no such table", "does not exist", "UndefinedTable")):
|
||||
return ("controller DB reachable but not initialized yet — "
|
||||
"start the controller (its master creates the schema on boot)")
|
||||
return f"controller DB query failed: {exc}"
|
||||
|
||||
|
||||
# ─── Forgejo API (read-only) ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -646,7 +658,7 @@ def _api_prs(state: str | None) -> dict[str, Any]:
|
||||
]
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"rows": [], "states": [],
|
||||
"note": f"controller DB query failed: {exc}"}
|
||||
"note": _controller_db_note(exc)}
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for issue in body:
|
||||
@@ -1844,12 +1856,20 @@ def _api_cost(days: int, group_by: str = "model") -> dict[str, Any]:
|
||||
provider, model, prices, in_tok, out_tok, cached,
|
||||
)
|
||||
key = _price_key(provider, model)
|
||||
# Resolved per-1M-token rates (same lookup chain _cost_usd
|
||||
# uses) so the UI can show how each row's cost was derived.
|
||||
p = prices.get(key) or prices.get(model) or prices.get("_unknown", {})
|
||||
rows.append(
|
||||
{
|
||||
"model": model, "provider": provider, "n": r["n"],
|
||||
"tokens_in": in_tok, "tokens_out": out_tok,
|
||||
"cached": cached, "usd": usd,
|
||||
"priced": (key in prices or model in prices),
|
||||
"price": {
|
||||
"in": p.get("in", 0.0),
|
||||
"out": p.get("out", 0.0),
|
||||
"cached_in": p.get("cached_in", p.get("in", 0.0)),
|
||||
},
|
||||
}
|
||||
)
|
||||
total_usd += usd
|
||||
@@ -2171,7 +2191,7 @@ def _api_attention() -> dict[str, Any]:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"by_state": [], "reaped_recent": [],
|
||||
"note": f"controller DB query failed: {exc}",
|
||||
"note": _controller_db_note(exc),
|
||||
"instrumentation_pending": True,
|
||||
}
|
||||
return {
|
||||
@@ -2246,7 +2266,7 @@ def _api_workflows(state: str | None) -> dict[str, Any]:
|
||||
).all()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"rows": [], "states": [],
|
||||
"note": f"controller DB query failed: {exc}",
|
||||
"note": _controller_db_note(exc),
|
||||
"instrumentation_pending": True}
|
||||
return {
|
||||
"rows": [
|
||||
@@ -2317,7 +2337,7 @@ def _api_workflow_events(workflow_id: int) -> dict[str, Any]:
|
||||
).all()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"workflow": None, "events": [], "attempts": [],
|
||||
"note": f"controller DB query failed: {exc}"}
|
||||
"note": _controller_db_note(exc)}
|
||||
return {
|
||||
"workflow": {
|
||||
"workflow_id": int(wf.workflow_id),
|
||||
|
||||
@@ -224,6 +224,25 @@ def test_api_workflows_empty_without_db_url(telemetry, monkeypatch):
|
||||
assert payload["instrumentation_pending"] is True
|
||||
|
||||
|
||||
def test_api_workflows_uninitialized_db_returns_friendly_note(
|
||||
telemetry, tmp_path, monkeypatch
|
||||
):
|
||||
"""A controller DB that exists but has no schema yet (the controller
|
||||
has never run create_all) degrades to a friendly note — not a raw
|
||||
SQL 'no such table' dump."""
|
||||
server, _cache = telemetry
|
||||
db_path = tmp_path / "empty_controller.sqlite"
|
||||
db_path.touch() # 0-byte file is a valid empty SQLite DB
|
||||
monkeypatch.setenv("CLEVERAGENTS_DB_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setattr(server, "_CONTROLLER_ENGINE", None)
|
||||
monkeypatch.setattr(server, "_CONTROLLER_ENGINE_URL", None)
|
||||
payload = server._api_workflows(None)
|
||||
assert payload["rows"] == []
|
||||
assert payload["instrumentation_pending"] is True
|
||||
assert "not initialized" in payload["note"]
|
||||
assert "no such table" not in payload["note"]
|
||||
|
||||
|
||||
def test_api_workflows_lists_seeded_workflows_with_state_summary(controller_db):
|
||||
"""Seeded workflows round-trip with their attempt counts, and the
|
||||
state summary aggregates every workflow regardless of the filter."""
|
||||
|
||||
Reference in New Issue
Block a user