feat(dashboard): LLM stats answer who/what spends tokens — member scope chips and spend breakdowns
Nightly Build / build (push) Successful in 4m27s
Nightly Build / build (push) Successful in 4m27s
The stats section was designed single-user: four global charts, no attribution. Request metadata rows now carry the session's source and the frame's agent_id/depth (additive ensure_column), denormalized at log time by the LoggingModel from the owner's pool — off the turn's hot path, degrading to NULLs, never to a lost row. The dashboard gains member scope chips filtering every chart, and a breakdown row splitting the range's billed tokens by member, kind (chat / sub-agents / cron / system agents / channels), agent, model and provider — a sub-agent's spend is attributed to the sub-agent itself. Rows predating the columns group under 'older data'.
This commit is contained in:
@@ -52,7 +52,7 @@ Two independent things have to be true, and both were violated at some point:
|
||||
| `copilot-render.js` | (helpers) | `renderMsg`, `renderTool`, `renderDiff`, etc. — shared by copilot and chat-page |
|
||||
| `sidebar.js` | `<app-sidebar>` | Nav sidebar; role-driven (`ui_mode`); inbox badge is **live** — the chat WS forwards the inbox lifecycle events (`approval_requested/resolved`, `clarification_*`, `elicitation_*`) regardless of `source`, `chat-session.js` re-dispatches them as the `inbox-changed` window event, and the sidebar (+ `agent-inbox.js`) refreshes on it; a 60 s poll remains as fallback |
|
||||
| `topbar.js` | `<app-topbar>` | Top nav bar; per-user avatar color hashed from the username |
|
||||
| `dashboard-page.js` | `<dashboard-page>` | `#dashboard` — status hero, LLM stats charts, pending inbox, quick guide |
|
||||
| `dashboard-page.js` | `<dashboard-page>` | `#dashboard` — status hero, LLM stats (member-scope chips + spend breakdowns by member/kind/agent/model/provider over `llm_requests` attribution columns), pending inbox, quick guide |
|
||||
| `shared/file-viewer-base.js` | `FileViewerBase` (base) | Shared file-viewer engine (fetch, kind detection, markdown/PDF/SVG/LaTeX/word-docs, watcher, `_renderBody`); driven by `_show`/`_hide`. Extended by desktop + mobile |
|
||||
| `file-viewer-page.js` | `<file-viewer-page>` | Desktop file viewer: `FileViewerBase` + hash routing via `window.openFile(path)` → `#file_viewer?path=...` |
|
||||
| `shared/file-viewer-mobile.js` | `<mobile-file-viewer-page>` | Mobile file viewer: `FileViewerBase` + prop-driven (`visible`/`path`), full-screen with back button |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## The client layer (`crates/skald-core/src/llm/`)
|
||||
|
||||
LLM client abstraction (OpenAI-compat, Anthropic, Ollama…). OpenAI-compatible provider *types* are runtime data, not code: `providers/declared.rs` loads `providers.yaml` at boot (see Config in [../CLAUDE.md](../CLAUDE.md)); only non-OpenAI-compatible or bespoke providers (anthropic, ollama, openai, openrouter) stay native. **Retriability** (`Model::is_retriable`, `agent-loop`) keys on the real HTTP status carried by `ModelError { status }`, **not** a substring of the message — a model id/token count containing "404"/"401" cannot mis-classify; 401/403/404/422 don't retry, 400/429/5xx/network do. **Request logging** is the `logging.rs::LoggingModel` decorator, attached by the *caller's* `ModelSelector` (`loop_adapters/selector.rs::SkaldSelector::with_log`) — never by `LlmManager`, which builds one shared client per model and cannot know whose traffic it serves. The decorator's `RequestLogTarget` carries the owner: metadata → `llm_requests` in the registry (`user_id`, the column the UI filters on), payload bodies/headers → `llm_request_payloads` in that user's own encrypted DB, keyed by `request_id`; session + frame come from the request's own `conversation`/`frame`, so kernel rounds, sub-agent frames and compaction summaries are all attributed with no extra plumbing (`ModelRequest::log` is unused here)
|
||||
LLM client abstraction (OpenAI-compat, Anthropic, Ollama…). OpenAI-compatible provider *types* are runtime data, not code: `providers/declared.rs` loads `providers.yaml` at boot (see Config in [../CLAUDE.md](../CLAUDE.md)); only non-OpenAI-compatible or bespoke providers (anthropic, ollama, openai, openrouter) stay native. **Retriability** (`Model::is_retriable`, `agent-loop`) keys on the real HTTP status carried by `ModelError { status }`, **not** a substring of the message — a model id/token count containing "404"/"401" cannot mis-classify; 401/403/404/422 don't retry, 400/429/5xx/network do. **Request logging** is the `logging.rs::LoggingModel` decorator, attached by the *caller's* `ModelSelector` (`loop_adapters/selector.rs::SkaldSelector::with_log`) — never by `LlmManager`, which builds one shared client per model and cannot know whose traffic it serves. The decorator's `RequestLogTarget` carries the owner: metadata → `llm_requests` in the registry (`user_id`, the column the UI filters on), payload bodies/headers → `llm_request_payloads` in that user's own encrypted DB, keyed by `request_id`; session + frame come from the request's own `conversation`/`frame`, so kernel rounds, sub-agent frames and compaction summaries are all attributed with no extra plumbing (`ModelRequest::log` is unused here). The metadata row also carries **denormalized attribution** for the dashboard's spend breakdowns — `source`, `agent_id`, `depth` — resolved by the spawned insert task from the owner's pool (`chat_sessions` + `chat_sessions_stack`; the frame row supplies the sub-agent's own id for child frames). That resolution is the only read the log path ever does, stays off the turn's hot path, and any failure degrades to NULLs — a missing attribution must never cost the row. Rows pre-dating the columns read as the stats' "older data" bucket.
|
||||
|
||||
## `providers.yaml` — two traps in the model metadata
|
||||
|
||||
|
||||
Reference in New Issue
Block a user