neuralwatt-router-service #8

Merged
alee merged 15 commits from neuralwatt-router-service into main 2026-08-29 17:17:54 +00:00
51 changed files with 11897 additions and 6 deletions

View File

@@ -1139,6 +1139,66 @@ The same applies to an Ollama shared over a VPN — it has no auth either, so
`0.0.0.0`, which would publish it on whatever network the client happens to
be on.
### A bare SIGTERM could hang the process forever — fixed 2026-08-29
Observed live: the dispatcher went unreachable (opencode: `ConnectionError:
... Connection refused`, retrying) while `systemctl --user status` still
reported it `active (running)`. The process was alive — it had logged
`Shutting down` / `Waiting for connections to close` and never got past that.
Its listening socket was already closed, but the SSE clients holding
`/events/decisions` open (the TUI, the admin dashboard) never disconnect, so
uvicorn's graceful shutdown had nothing to wait *out*.
That alone would just be slow. What made it fatal: there was no matching
`Stopping Local LLM model router...` line from systemd before it — something
sent SIGTERM straight to the PID rather than through `systemctl
stop`/`restart`. Systemd only enforces `TimeoutStopUSec` (10s here) when *it*
is the one running the stop job; a signal delivered outside that path leaves
the unit `active (running)` forever — the main PID never exits, so
`Restart=on-failure` never fires either. The process just sat there,
permanently unreachable, with no supervisor noticing anything was wrong.
The source of that bare SIGTERM is still unknown — nothing in
`dispatcher.py`, `admin.py`, or `tui.py` sends a signal to the process, so it
was most likely a manual `kill` / `pkill` / process-manager action in another
terminal while iterating on the admin portal work. Worth checking for if it
recurs.
**Fixed the failure mode, not just the trigger.** `ExecStart` now passes
`--timeout-graceful-shutdown 5` (`deploy/llm-router.service`), which caps
uvicorn's own connection-draining wait at 5s *regardless of who sends the
signal or whether systemd is tracking a stop job*. A bare SIGTERM now
converges to a clean exit in 5s instead of hanging forever. This also fixes
routine restarts: every ordinary `systemctl --user restart` was silently
taking the full 10s-then-SIGKILL path already, because the same open SSE
connections were blocking graceful shutdown there too — it just didn't
matter, because systemd was tracking that stop job and enforcing the kill.
Recovery if this happens again: `systemctl --user restart
llm-router.service`. Since the hung process was never in a tracked stop job,
this issues a fresh stop/start cycle that systemd *does* enforce the timeout
on, so it reliably clears the hang.
**Follow-up, same day: the graceful-shutdown fix exposed a second bug.**
Once the process could exit cleanly on a bare SIGTERM instead of hanging,
systemd's `Restart=on-failure` turned out to explicitly exclude clean
termination by SIGTERM/SIGINT from auto-restart — its assumption is that
SIGTERM always means someone deliberately asked the service to stop. That
assumption is false for this bare signal, so the fixed process was now
exiting cleanly and then just staying `inactive (dead)` until a human
noticed. Changed `Restart=on-failure` → `Restart=always` in both
`deploy/llm-router.service` and the live unit; `systemctl stop`/`restart`
are still honored correctly regardless of that policy (systemd tracks
deliberate stops separately from the Restart= decision), so this only adds
self-healing for the unexplained case.
The signal's actual source is still open — live investigation and audit
trail in `code_plans/router-unreachable-signal-investigation.md`. Confirmed
so far: it is not `systemctl`, not the admin portal's restart trigger, not
suspend/resume, not the OOM killer, and — via `auditd` — not delivered
through the `kill` or `tgkill` syscalls either, which is why the watch was
extended to `pidfd_send_signal` and the `rt_*sigqueueinfo` syscalls.
## Pointing a coding agent at it
The `/v1` endpoints are OpenAI-compatible, so any normal client works —

View File

@@ -1,4 +1,7 @@
# Local LLM Model Router
<h1 align="center">
<img src="assets/6krrt-logo.svg" width="120" alt="6krrt logo"><br>
6krrt — Local LLM Model Router
</h1>
This router sits between a coding agent (opencode, an SDK, plain curl — any
OpenAI-compatible client) and the cloud LLMs it calls. It uses a local model
@@ -41,6 +44,7 @@ measurement is how you check whether it still holds for you.
- [Installation](#installation)
- [Local (`venv`)](#local-venv)
- [As a systemd service](#as-a-systemd-service)
- [Where Ollama lives](#where-ollama-lives)
- [Usage](#usage)
- [Route without spending anything](#route-without-spending-anything)
- [Skip the classifier when you already know the shape](#skip-the-classifier-when-you-already-know-the-shape)
@@ -71,13 +75,11 @@ measurement is how you check whether it still holds for you.
- [What routing actually returns, and why it moves](#what-routing-actually-returns-and-why-it-moves)
- [API Endpoints](#api-endpoints)
- [Logging and Traceability](#logging-and-traceability)
- [Scheduled Jobs (systemd)](#scheduled-jobs-systemd)
- [Self-Eval Harness (`eval_proficiency.py`)](#self-eval-harness-eval_proficiencypy)
- [Classifier Reliability Notes](#classifier-reliability-notes)
- [Pointing a Coding Agent at It](#pointing-a-coding-agent-at-it)
- [Testing](#testing)
- [Setup](#setup)
- [Where Ollama lives](#where-ollama-lives)
- [Advanced: Fitting local models into 24GB VRAM](#advanced-fitting-local-models-into-24gb-vram)
- [Known Limitations & Open Items](#known-limitations--open-items)
## Requirements
@@ -142,6 +144,33 @@ This now feeds `eco` only. Cost is priced per request from catalog prices, so
routing no longer depends on the sweep at all; disabling this timer costs you
carbon figures, not routing quality.
### Where Ollama lives
```bash
ollama pull mistral-nemo:12b # or whatever you set as classifier.model
```
It does not have to be on the machine running the router; the box with the
GPU usually isn't the laptop. To use one across a VPN, point **both**
endpoints at it:
```yaml
classifier:
base_url: "http://<vpn-ip>:11434/v1"
verification:
base_url: "http://<vpn-ip>:11434" # same host, so `model` can stay null
```
and apply `deploy/ollama-over-vpn.conf` on the serving host — Ollama binds
`127.0.0.1` by default and will otherwise refuse. Bind it to the VPN address
rather than `0.0.0.0`: Ollama has no authentication, so anything reaching the
port can run inference and enumerate your models.
Both endpoints move together because the verifier speaks Ollama's *native*
API and cannot follow the classifier to a cloud provider. Config load refuses
the case where they are on different hosts and `verification.model` is null,
because that combination fails silently.
## Usage
### Route without spending anything
@@ -1070,6 +1099,37 @@ config as arguments, so the suite runs offline on a clean checkout.
| `test_classifier_input.py` | Classifier framing: `_previous_context` scope, `_classifier_user_content` framing |
| `test_route_decisions.py` | `route_decisions` table, inline-create helper, config gate |
## Advanced: Fitting local models into 24GB VRAM
Ollama loads `mistral-nemo:12b` and `qwen3-vl:4b` at the context size embedded in their library tags unless a custom tag overrides it. On a 24GB card that default context is expensive: `mistral-nemo:12b` came up at ~14GB (`num_ctx=32768`), and `qwen3-vl:4b` came up at ~9.4GB (`num_ctx=32768`). Running both together left only ~1.9GB of headroom.
You cannot fix this per request. The OpenAI-compatible `/v1/chat/completions` endpoint in Ollama 0.22.0 was verified live with every field shape that seemed plausible, and in every case it returned 200 while silently keeping the loaded context unchanged. The context size has to be baked into the model tag itself with a `Modelfile`:
```bash
# Router uses this for classification and verification (same resident model)
printf 'FROM mistral-nemo:12b\nPARAMETER num_ctx 8192\n' > Modelfile.router
ollama create mistral-nemo-router:12b -f Modelfile.router
# Vision fallback only
printf 'FROM qwen3-vl:4b\nPARAMETER num_ctx 16384\n' > Modelfile.vision
ollama create qwen3-vl-router:4b -f Modelfile.vision
```
`8192` is deliberately conservative for classification and verification. It leaves about 2× headroom over the real working set, and it keeps the classifier and verifier on the same resident instance rather than risking two differently-sized copies of the same base model. `16384` is a conservative cut for the raw message list the vision fallback receives, because that path is called before context pruning.
With these tags the combined resident footprint was measured at roughly **15.9GB** in the worst case: classifier and verifier share one `mistral-nemo-router:12b` instance at ~8.6GB, with the `qwen3-vl-router:4b` vision fallback loaded alongside it. That leaves real headroom on a 24GB card.
`config.yaml` already points at these tags by default:
```yaml
classifier:
model: mistral-nemo-router:12b
verification:
model: mistral-nemo-router:12b
local_vision:
model: qwen3-vl-router:4b
```
## Known Limitations & Open Items
- **Leaderboard priors unfilled** — `leaderboards.yaml` ships empty. `python leaderboard.py --check`

802
admin.py Normal file
View File

@@ -0,0 +1,802 @@
"""Admin endpoints for the router: /admin/api/health and /admin/api/snapshot.
This module MUST NOT import ``dispatcher`` — it mirrors ``metrics.py``'s
contract (explicit ``(conn, cfg)`` args, no module-level globals, no import of
the service module) and is mounted onto the dispatcher app by
``dispatcher.app.include_router(admin.build_router(cfg, _db))``.
Like ``metrics.py``, it never touches the TUI, never widens the bind, and
never exposes prompts, session_dir, or conversation text. It is loopback-bound
exactly like /health and /metrics.
``build_router`` takes a ``_db_callable`` factory (returning an
``sqlite3.Connection`` with ``row_factory`` set) so the module owns nothing
global; the caller supplies its own connection source at mount time.
"""
from __future__ import annotations
import asyncio
import os
import shutil
import sqlite3
import subprocess
import sys
import threading
import time
import uuid
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, List, Optional
from fastapi import APIRouter, BackgroundTasks, HTTPException
from fastapi.responses import FileResponse
from openai import OpenAI, OpenAIError
from pydantic import BaseModel, ValidationError
from ruamel.yaml import YAML
import metrics
from config import FlexPreference, RouterConfig, load_config
# Named /api/history ranges -> (span_seconds, bucket_seconds). Buckets floor
# an observed_at to index = unix_seconds // bucket, so the representative ts of
# each bucket is a whole multiple of bucket_seconds.
_ADMIN_HISTORY_RANGES = {
"1h": (3600, 300),
"6h": (21600, 900),
"24h": (86400, 3600),
"7d": (604800, 21600),
"30d": (2592000, 86400),
}
# Column expression for the non-count energy series. All are energy_observations
# sums; the keys line up with the /api/history response series.
_HISTORY_ENERGY_SERIES = {
"requests_per_bucket": "COUNT(*)",
"cost_per_bucket": "COALESCE(SUM(cost_usd), 0)",
"energy_per_bucket": "COALESCE(SUM(energy_kwh), 0)",
"carbon_per_bucket": "COALESCE(SUM(carbon_g_co2eq), 0)",
}
def ensure_admin_tables(conn):
"""Idempotently create the admin_model_overrides table.
Mirrors ``ensure_route_decisions`` in dispatcher.py: CREATE TABLE IF NOT
EXISTS + CREATE INDEX IF NOT EXISTS, both safe to call repeatedly.
"""
conn.execute(
"CREATE TABLE IF NOT EXISTS admin_model_overrides ("
" model_id TEXT NOT NULL,"
" provider TEXT NOT NULL,"
" availability TEXT NOT NULL,"
" reason TEXT,"
" updated_at TEXT NOT NULL,"
" PRIMARY KEY (model_id, provider)"
")"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_admin_model_overrides_availability "
"ON admin_model_overrides (availability)"
)
conn.commit()
def _history_series(
conn: sqlite3.Connection,
span: int,
bucket: int,
table: str,
aggregate: str,
) -> List[list]:
"""Bucketed GROUP BY over *table* with the given *aggregate* expression.
Returns ``[[unix_ts, value], ...]`` ascending, one row per bucket that has
rows inside the last *span* seconds; buckets with no rows are omitted, so an
empty table yields ``[]``. ``table`` and ``aggregate`` are only ever module
literals (never user input), so they are safe to interpolate.
"""
rows = conn.execute(
f"""
SELECT CAST((julianday(observed_at) - julianday('1970-01-01')) * 86400.0
/ ? AS INTEGER) * ? AS bucket_ts,
{aggregate} AS value
FROM {table}
WHERE julianday(observed_at) >= julianday('now') - ? / 86400.0
GROUP BY CAST((julianday(observed_at) - julianday('1970-01-01'))
* 86400.0 / ? AS INTEGER)
ORDER BY bucket_ts
""",
(bucket, bucket, span, bucket),
).fetchall()
return [[row["bucket_ts"], row["value"]] for row in rows]
def _classifier_reachable(cfg: Any) -> bool:
"""Whether the classifier endpoint answers a models.list() probe.
Replicates dispatcher's /health logic: ``api_key_env`` unset means the
unauthenticated local Ollama case (SDK still needs a key, so "ollama" is
used), and any OpenAIError (including a connection refusal) is treated as
unreachable.
"""
key_env = cfg.classifier.api_key_env
api_key = os.environ.get(key_env) if key_env else "ollama"
client = OpenAI(base_url=cfg.classifier.base_url, api_key=api_key, max_retries=0)
try:
client.models.list()
return True
except OpenAIError:
return False
def _health_check(conn: sqlite3.Connection, cfg: Any) -> dict:
"""The same diagnostics /health reports, as a dict for snapshot to embed."""
counts = dict(
conn.execute(
"""
SELECT 'models', COUNT(*) FROM models
UNION ALL SELECT 'routable', COUNT(*) FROM models
WHERE access_level = 'public' AND availability = 'active'
UNION ALL SELECT 'proficiency', COUNT(*) FROM proficiency
UNION ALL SELECT 'energy_observations', COUNT(*) FROM energy_observations
"""
).fetchall()
)
return {
"status": "ok",
"counts": counts,
"scoring": metrics.scoring_coverage(conn, cfg),
"classifier_reachable": _classifier_reachable(cfg),
"classifier_model": cfg.classifier.model,
"tiers": cfg.tiers,
"providers": list(cfg.dispatch_providers),
"api_keys_present": {
name: bool(os.environ.get(p.api_key_env))
for name, p in cfg.dispatch_providers.items()
},
}
# Scalar fields a model detail exposes to the admin UI, renamed from the SQLite
# column names to the JSON key names baked into the response contract. The
# explicit rename list (rather than ``dict(row)``) guarantees we never leak
# internal columns (e.g. poller/bookkeeping fields) into the response.
_MODEL_FIELDS: dict[str, str] = {
"model_id": "model_id",
"provider": "provider",
"base_model_id": "base_model_id",
"display_name": "display_name",
"availability": "availability",
"tier": "tier",
"context_window": "context_window",
"effective_context_window": "effective_context_window",
"latency_class": "latency_class",
"reasoning_mode": "reasoning_mode",
"context_variant": "context_variant",
"access_level": "access_level",
"supports_tools": "supports_tools",
"supports_json_mode": "supports_json_mode",
"supports_vision": "supports_vision",
"supports_reasoning": "supports_reasoning",
"reasoning_default_enabled": "reasoning_default_enabled",
"cost_per_1m_prompt": "cost_per_1m_prompt",
"cost_per_1m_completion": "cost_per_1m_completion",
}
def _model_rows(conn: sqlite3.Connection) -> List[dict]:
"""All models with their proficiency map, in the admin JSON contract.
Proficiency is a LEFT JOIN from ``models`` to ``proficiency`` grouped by
category so a model with no proficiency rows still appears (its map is
empty). Overrides from ``admin_model_overrides`` are merged so every
returned model object carries ``effective_availability`` (which may differ
from the DB ``availability``), ``is_overridden`` (bool), and the raw
``availability`` column. Internal columns never escape.
"""
# Load overrides into a (model_id, provider) -> availability lookup.
overrides = {}
for row in conn.execute(
"SELECT model_id, provider, availability FROM admin_model_overrides"
):
overrides[(row["model_id"], row["provider"])] = row["availability"]
rows = conn.execute(
"""
SELECT m.model_id,
m.provider,
m.base_model_id,
m.display_name,
m.availability,
m.tier,
m.context_window,
m.effective_context_window,
m.latency_class,
m.reasoning_mode,
m.context_variant,
m.access_level,
m.supports_tools,
m.supports_json_mode,
m.supports_vision,
m.supports_reasoning,
m.reasoning_default_enabled,
m.cost_per_1m_prompt,
m.cost_per_1m_completion,
p.category,
p.blended_score
FROM models m
LEFT JOIN proficiency p
ON p.model_id = m.model_id AND p.provider = m.provider
AND p.blended_score IS NOT NULL
ORDER BY m.model_id, m.provider, p.category
"""
).fetchall()
models: dict[tuple[str, str], dict] = {}
order: list[tuple[str, str]] = []
for row in rows:
key = (row["model_id"], row["provider"])
if key not in models:
entry = {
json_key: row[col]
for col, json_key in _MODEL_FIELDS.items()
}
for bool_col in (
"supports_tools",
"supports_json_mode",
"supports_vision",
"supports_reasoning",
"reasoning_default_enabled",
):
entry[bool_col] = bool(entry[bool_col])
entry["proficiency"] = {}
override_avail = overrides.get(key)
entry["effective_availability"] = (
override_avail if override_avail else row["availability"]
)
entry["is_overridden"] = key in overrides
models[key] = entry
order.append(key)
if row["category"] is not None:
models[key]["proficiency"][row["category"]] = row["blended_score"]
return [models[key] for key in order]
# --- runtime toggle knobs ------------------------------------------------------
# Each boolean knob maps to a dotted attribute path on RouterConfig. The GET
# response keys differ from the POST knob names only for circuit_breaker, which
# is reported as a nested {"enabled": ...} object. Values are NEVER persisted
# to config.yaml and never touch secrets, model names, or URLs.
_BOOL_KNOBS: dict[str, tuple[str, ...]] = {
"log_route_decisions": ("logging", "log_route_decisions"),
"log_energy_observations": ("logging", "log_energy_observations"),
"circuit_breaker_enabled": ("circuit_breaker", "enabled"),
"local_llm_enabled": ("verification", "local_llm_enabled"),
"session_cache_enabled": ("session_cache", "enabled"),
"pinch_enabled": ("pinch", "enabled"),
"pinch_relevance_enabled": ("pinch", "relevance", "enabled"),
}
_FLEX_KNOB = "default_flex_preference"
_FLEX_PATH: tuple[str, ...] = ("routing", "default_flex_preference")
_FLEX_VALUES = frozenset(v.value for v in FlexPreference)
# --- persisted config allowlist -------------------------------------------------
# Dotted config.yaml paths an operator is allowed to edit. Everything else —
# classifier/verification/local_vision URLs and model names, api_key_env,
# provider blocks, dispatch_providers, and secrets — is deliberately OFF this
# list. Editing works only on the named scalars. Writes go through
# comment-preserving ruamel.yaml round-trip and are validated via ``RouterConfig``
# before any byte reaches disk; a backup is made first.
_CONFIG_ALLOWLIST: dict[str, tuple[str, ...]] = {
"logging.level": ("logging", "level"),
"objective.quality_tolerance": ("objective", "quality_tolerance"),
"objective.max_energy_per_request": ("objective", "max_energy_per_request"),
"objective.plan_kwh_per_period": ("objective", "plan_kwh_per_period"),
"circuit_breaker.enabled": ("circuit_breaker", "enabled"),
"session_cache.enabled": ("session_cache", "enabled"),
"verification.local_llm_enabled": ("verification", "local_llm_enabled"),
"pinch.enabled": ("pinch", "enabled"),
"pinch.relevance.enabled": ("pinch", "relevance", "enabled"),
"routing.default_flex_preference": ("routing", "default_flex_preference"),
}
# Order preserves config.yaml layout for the GET response.
_CONFIG_GET_ORDER: list[str] = [
"logging.level",
"objective.quality_tolerance",
"objective.max_energy_per_request",
"objective.plan_kwh_per_period",
"circuit_breaker.enabled",
"session_cache.enabled",
"verification.local_llm_enabled",
"pinch.enabled",
"pinch.relevance.enabled",
"routing.default_flex_preference",
]
# Serializes the load/validate/backup/write sequence of the persisted-config
# endpoints so concurrent writes cannot interleave and truncate config.yaml.
_config_write_lock = threading.Lock()
def _dict_get_at(store: Any, path: tuple[str, ...]) -> Any:
"""Walk a dotted *path* (tuple) through a plain-nested mapping."""
cur = store
for part in path:
cur = cur[part]
return cur
def _dict_set_at(store: Any, path: tuple[str, ...], value: Any) -> None:
"""Set *value* at dotted *path* in a plain-nested mapping."""
cur = store
for part in path[:-1]:
cur = cur[part]
cur[path[-1]] = value
def load_config_store(config_path: Path) -> Any:
"""Load *config_path* as a ruamel CommentedMap so comments survive a dump."""
yaml = YAML()
yaml.preserve_quotes = True
return yaml.load(config_path.read_text())
def _persist_config_value(
config_path: Path, path: tuple[str, ...], value: Any
) -> None:
"""Atomically persist *value* at dotted *path* in *config_path*.
The load/validate/backup/write sequence runs under the module-level
``_config_write_lock`` so concurrent writes cannot interleave. The write is
a ruamel.yaml round-trip (comments survive), the WHOLE config is re-
validated via ``RouterConfig`` before any byte touches disk, and the on-
disk replacement uses a ``.tmp`` file + ``os.replace`` so a reader never
observes a truncated config.yaml. Raises ``ValidationError`` (leaving no
backup on disk) if the candidate config is invalid.
"""
with _config_write_lock:
store = load_config_store(config_path)
_dict_set_at(store, path, value)
RouterConfig(**store)
backup = config_path.with_name(
f"config.yaml.bak.{int(time.time())}"
)
shutil.copyfile(config_path, backup)
tmp = config_path.with_suffix(config_path.suffix + ".tmp")
with tmp.open("w") as fh:
YAML().dump(store, fh)
os.replace(tmp, config_path)
class _AvailabilityBody(BaseModel):
availability: str
reason: Optional[str] = None
class _ValueBody(BaseModel):
"""A single runtime knob value: a bool for boolean knobs, a flex string
for ``default_flex_preference``. Type-checked at validation time."""
value: Any
def _get_at(cfg: Any, path: tuple[str, ...]) -> Any:
cur = cfg
for part in path:
cur = getattr(cur, part)
return cur
def _set_at(cfg: Any, path: tuple[str, ...], value: Any) -> None:
cur = cfg
for part in path[:-1]:
cur = getattr(cur, part)
setattr(cur, path[-1], value)
def _runtime_state(cfg: Any) -> dict:
"""Read every toggle knob's current in-memory value off ``cfg``."""
return {
"log_route_decisions": _get_at(cfg, _BOOL_KNOBS["log_route_decisions"]),
"log_energy_observations": _get_at(
cfg, _BOOL_KNOBS["log_energy_observations"]
),
"circuit_breaker": {
"enabled": _get_at(cfg, _BOOL_KNOBS["circuit_breaker_enabled"])
},
"local_llm_enabled": _get_at(cfg, _BOOL_KNOBS["local_llm_enabled"]),
"session_cache_enabled": _get_at(cfg, _BOOL_KNOBS["session_cache_enabled"]),
"pinch_enabled": _get_at(cfg, _BOOL_KNOBS["pinch_enabled"]),
"pinch_relevance_enabled": _get_at(
cfg, _BOOL_KNOBS["pinch_relevance_enabled"]
),
"default_flex_preference": _get_at(cfg, _FLEX_PATH).value,
}
def build_router(
cfg: Any,
_db_callable: Callable[[], sqlite3.Connection],
base_dir: Optional[str] = None,
) -> APIRouter:
"""Build the admin APIRouter bound to the caller's config and DB factory.
``base_dir`` is the repo root (the directory holding config.yaml and the
maintenance scripts). The persisted-config endpoints use it to locate
``config.yaml`` for comment-preserving writes; the maintenance triggers use
it as their spawn CWD. Defaults to this module's parent (the repo root), so
the router is portable and testable without an explicit base_dir.
"""
_module_dir = Path(__file__).resolve().parent
config_path = (
Path(base_dir) / "config.yaml"
if base_dir is not None
else _module_dir / "config.yaml"
)
router = APIRouter()
_admin_frontend = _module_dir / "admin" / "frontend" / "index.html"
@router.get("/")
def admin_index() -> FileResponse:
return FileResponse(_admin_frontend, media_type="text/html")
@router.get("/api/health")
def admin_health() -> dict:
return {"status": "ok"}
@router.get("/api/models")
def admin_models() -> list:
"""All models with per-category proficiency, for the admin model table."""
conn = _db_callable()
try:
return _model_rows(conn)
finally:
conn.close()
@router.get("/api/models/{model_id}/{provider}")
def admin_model_detail(model_id: str, provider: str) -> dict:
"""A single model's detail (same shape as one /api/models element)."""
conn = _db_callable()
try:
for row in _model_rows(conn):
if row["model_id"] == model_id and row["provider"] == provider:
return row
raise HTTPException(status_code=404, detail="model not found")
finally:
conn.close()
@router.post("/api/models/{model_id}/{provider}/availability")
def admin_set_availability(
model_id: str,
provider: str,
body: _AvailabilityBody,
) -> dict:
"""Upsert an admin override for a model's availability.
Validates that availability is one of active/deprecated/stale.
Returns 404 if the model_id/provider pair does not exist in the
models table, 422 for an invalid availability value.
"""
# Validate availability value (Pydantic only typed as str; keep explicit
# allow-list so unknown values get a clear 422).
valid_avail = {"active", "deprecated", "stale"}
avail_val = body.availability
reason_val = body.reason
if avail_val not in valid_avail:
raise HTTPException(
status_code=422,
detail=f"availability must be one of {sorted(valid_avail)}, got {avail_val!r}",
)
conn = _db_callable()
try:
# Check model exists.
exists = conn.execute(
"SELECT 1 FROM models WHERE model_id=? AND provider=?",
(model_id, provider),
).fetchone()
if not exists:
raise HTTPException(status_code=404, detail="model not found")
now = datetime.now(timezone.utc).isoformat()
conn.execute(
"""
INSERT INTO admin_model_overrides
(model_id, provider, availability, reason, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(model_id, provider) DO UPDATE SET
availability = excluded.availability,
reason = excluded.reason,
updated_at = excluded.updated_at
""",
(model_id, provider, avail_val, reason_val, now),
)
conn.commit()
# Return the updated model detail (now carries effective_availability).
for row in _model_rows(conn):
if row["model_id"] == model_id and row["provider"] == provider:
return row
# Should not happen since we already checked existence.
raise HTTPException(status_code=500, detail="unexpected: row lost after upsert")
finally:
conn.close()
@router.delete("/api/models/{model_id}/{provider}/availability")
def admin_delete_availability(model_id: str, provider: str) -> dict:
"""Delete an admin availability override for a model.
Returns 404 if the model does not exist. If no override row exists
for this model, still returns the model detail (no-op from routing
perspective).
"""
if isinstance(model_id, bytes):
model_id = model_id.decode()
if isinstance(provider, bytes):
provider = provider.decode()
conn = _db_callable()
try:
# Check model exists.
exists = conn.execute(
"SELECT 1 FROM models WHERE model_id=? AND provider=?",
(model_id, provider),
).fetchone()
if not exists:
raise HTTPException(status_code=404, detail="model not found")
conn.execute(
"DELETE FROM admin_model_overrides WHERE model_id=? AND provider=?",
(model_id, provider),
)
conn.commit()
for row in _model_rows(conn):
if row["model_id"] == model_id and row["provider"] == provider:
return row
raise HTTPException(status_code=500, detail="unexpected: row lost")
finally:
conn.close()
@router.get("/api/snapshot")
def admin_snapshot() -> dict:
"""Fused metrics + health + recent-history summary for the admin UI.
Covers analytics (quota, coverage, per-model aggregates, verdict mix,
top proficiency) plus a liveness/health snapshot, timestamped as
``generated_at``.
"""
conn = _db_callable()
try:
return {
"quota": metrics.quota_burn(conn, cfg),
"coverage": metrics.scoring_coverage(conn, cfg),
"recent_decisions": metrics.recent_decisions(conn),
"per_model": metrics.per_model(conn),
"verdict_mix": metrics.verdict_mix(conn),
"top_proficiency": metrics.top_proficiency(conn, "coding_general"),
"health": _health_check(conn, cfg),
"generated_at": datetime.now(timezone.utc).isoformat(),
}
finally:
conn.close()
@router.get("/api/history")
def admin_history(range: str = "24h") -> dict:
"""Bucketed time-series over energy/decision tables for the admin UI.
``range`` selects a (span, bucket) pair from ``_ADMIN_HISTORY_RANGES``.
Returns five series (decisions, requests, cost, energy, carbon) as
``[unix_ts, value]`` pairs occupying buckets that had rows inside the
span; empty tables yield ``[]`` for every series. Raises 400 for any
unlisted range value.
"""
if range not in _ADMIN_HISTORY_RANGES:
raise HTTPException(
status_code=400,
detail=f"unknown history range: {range!r}",
)
span, bucket = _ADMIN_HISTORY_RANGES[range]
conn = _db_callable()
try:
return {
"decisions_per_bucket": _history_series(
conn, span, bucket, "route_decisions", "COUNT(*)"
),
**{
name: _history_series(
conn, span, bucket, "energy_observations", aggregate
)
for name, aggregate in _HISTORY_ENERGY_SERIES.items()
},
}
finally:
conn.close()
# --- operational triggers ------------------------------------------------
# Maintenance scripts run as child processes. Running them with
# ``asyncio.create_subprocess_exec`` (never ``subprocess.run`` inline) keeps
# the event loop responsive. Commands run from the repo root (the directory
# holding admin.py) so relative ``config.yaml`` / ``router.db`` resolve.
_repo_root = str(Path(__file__).resolve().parent)
def _job(command: str, status: str, returncode, output_tail: str) -> dict:
"""The documented job object returned by every trigger endpoint."""
return {
"id": uuid.uuid4().hex[:12],
"command": command,
"status": status,
"returncode": returncode,
"output_tail": output_tail,
}
async def _run_steps(
steps: list[list[str]], timeout: float, cwd: str
) -> dict:
"""Run maintenance steps sequentially (``&&`` semantics), return a job.
Each step is spawned with ``asyncio.create_subprocess_exec``; output
(stdout merged with stderr) is capped to the last 2048 chars for
``output_tail``. A single wall-clock deadline covers the whole chain,
so a slow poller cannot quietly eat the whole budget and then leave a
late tier step hanging.
"""
command = " && ".join(" ".join(s) for s in steps)
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
collected: list[str] = []
proc = None
for step in steps:
try:
proc = await asyncio.create_subprocess_exec(
*step,
cwd=cwd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
except Exception as exc: # noqa: BLE001 - top-level spawn boundary
return _job(
command, "failed", None, f"spawn failed ({step[0]}): {exc}"[:2048]
)
remaining = deadline - loop.time()
try:
out, _ = await asyncio.wait_for(
proc.communicate(), timeout=max(0.0, remaining)
)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return _job(
command,
"timed_out",
None,
f"Command timed out after {int(timeout)}s",
)
if out:
collected.append(out.decode("utf-8", errors="replace"))
if proc.returncode != 0:
break # && semantics: stop on the first failing step
output = "".join(collected)
status = "success" if proc is not None and proc.returncode == 0 else "failed"
return _job(command, status, proc.returncode if proc else None, output[-2048:])
@router.post("/api/refresh-catalog")
async def admin_refresh_catalog() -> dict:
"""Run ``poller.py && tier.py`` to refresh the model catalog."""
return await _run_steps(
[[sys.executable, "poller.py"], [sys.executable, "tier.py"]],
timeout=120.0,
cwd=_repo_root,
)
@router.post("/api/seed-energy")
async def admin_seed_energy(samples: int = 5) -> dict:
"""Sweep the energy reference workload for ``samples`` per model."""
return await _run_steps(
[[sys.executable, "seed_energy.py", "--samples", str(samples)]],
timeout=600.0,
cwd=_repo_root,
)
@router.post("/api/apply-feedback")
async def admin_apply_feedback(dry_run: bool = False) -> dict:
"""Fold verification outcomes back into proficiency (optionally dry)."""
argv = [sys.executable, "feedback.py"]
if dry_run:
argv.append("--dry-run")
return await _run_steps([argv], timeout=120.0, cwd=_repo_root)
@router.post("/api/restart-service")
def admin_restart_service(background: BackgroundTasks) -> dict:
"""Schedule a systemctl restart and return immediately.
The systemctl call runs as a post-response BackgroundTask (fire-and-
forget) so the HTTP response flushes before the service process dies.
It is deliberately never awaited inline.
"""
background.add_task(_restart_service)
return {"status": "restarting"}
def _restart_service() -> None:
subprocess.run(
["systemctl", "--user", "restart", "llm-router.service"],
check=False,
capture_output=True,
)
@router.get("/api/runtime")
def admin_runtime_state() -> dict:
"""Persisted (config.yaml) vs runtime (in-memory) value of each knob."""
persisted = _runtime_state(load_config("config.yaml"))
runtime = _runtime_state(cfg)
return {
key: {"persisted": persisted[key], "runtime": runtime[key]}
for key in persisted
}
@router.post("/api/runtime/{knob}")
def admin_set_runtime_knob(knob: str, body: _ValueBody) -> dict:
"""Flip a single knob's in-memory value; config.yaml is never written."""
if knob in _BOOL_KNOBS:
if not isinstance(body.value, bool):
raise HTTPException(
status_code=422,
detail=f"{knob} expects a boolean value",
)
_set_at(cfg, _BOOL_KNOBS[knob], body.value)
return {"ok": True, knob: body.value}
if knob == _FLEX_KNOB:
if body.value not in _FLEX_VALUES:
raise HTTPException(
status_code=422,
detail=(
f"{knob} must be one of {sorted(_FLEX_VALUES)}, "
f"got {body.value!r}"
),
)
_set_at(cfg, _FLEX_PATH, FlexPreference(body.value))
return {"ok": True, knob: body.value}
raise HTTPException(
status_code=400,
detail=f"unknown runtime knob: {knob}",
)
@router.get("/api/config")
def admin_config_get() -> dict:
"""The allowlisted config.yaml values, as {dotted_key: value}."""
return {key: _dict_get_at(load_config_store(config_path), path)
for key, path in _CONFIG_ALLOWLIST.items()}
@router.post("/api/config/{key}")
def admin_config_write(key: str, body: _ValueBody) -> dict:
"""Persist one allowlisted value to config.yaml (comment-preserving)."""
if key not in _CONFIG_ALLOWLIST:
raise HTTPException(
status_code=403,
detail=f"config key is not editable: {key}",
)
try:
_persist_config_value(
config_path, _CONFIG_ALLOWLIST[key], body.value
)
except ValidationError as exc:
raise HTTPException(
status_code=422,
detail=exc.errors()[0]["msg"],
) from exc
return {
"key": key,
"value": body.value,
"message": "A restart is required for this change to take effect",
}
return router

View File

916
admin/frontend/index.html Normal file
View File

@@ -0,0 +1,916 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard — LLM Router</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<style>
/* ── Reset & Base ── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0a0e14;--surface:#111820;--surface-hover:#16202c;
--border:#233040;--border-bright:#2c4060;
--text:#c8d6e5;--text-dim:#5a6a7f;--text-bright:#e8f0f8;
--green:#2ecc71;--green-dim:#1a7a40;
--red:#e74c3c;--red-dim:#8a2a20;
--yellow:#f1c40f;--yellow-dim:#6d5a10;
--blue:#3498db;--blue-dim:#1a3a5c;
--cyan:#00d2d3;--purple:#9b59b6;--orange:#e67e22;
--font:'JetBrains Mono','Fira Code','Cascadia Code','SF Mono',Consolas,monospace;
--radius:4px;
}
html{font-size:13px;background:var(--bg);color:var(--text);font-family:var(--font)}
body{padding:12px;max-width:1600px;margin:0 auto}
h1{font-size:1.4rem;font-weight:600;color:var(--green);margin-bottom:4px;letter-spacing:0.02em}
h2{font-size:1rem;font-weight:600;color:var(--text-bright);margin-bottom:8px;display:flex;align-items:center;gap:6px}
h2 .icon{font-size:0.85em;opacity:0.7}
p{line-height:1.5}
a{color:var(--cyan);text-decoration:none}
a:hover{text-decoration:underline}
/* ── Layout ── */
.header{display:flex;justify-content:space-between;align-items:center;padding-bottom:8px;margin-bottom:12px;border-bottom:1px solid var(--border)}
.header-left{display:flex;align-items:baseline;gap:12px}
.header h1{margin:0}
.meta{color:var(--text-dim);font-size:0.8rem}
.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;animation:pulse 2s infinite}
.sse-connected{background:var(--green);box-shadow:0 0 6px var(--green-dim)}
.sse-disconnected{background:var(--red);box-shadow:0 0 6px var(--red-dim)}
.sse-connecting{background:var(--yellow);box-shadow:0 0 6px var(--yellow-dim)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
.dashboard{display:grid;gap:10px}
.two-col{grid-template-columns:1fr 1fr}
.three-col{grid-template-columns:1fr 1fr 1fr}
.one-col{grid-template-columns:1fr}
.two-thirds{grid-template-columns:2fr 1fr}
.half-half{grid-template-columns:1fr 1fr}
/* ── Panels ── */
.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;overflow:hidden}
.panel:hover{border-color:var(--border-bright)}
.panel-scroll{max-height:380px;overflow-y:auto;overscroll-behavior:contain}
.panel-scroll::-webkit-scrollbar{width:6px}
.panel-scroll::-webkit-scrollbar-track{background:var(--surface)}
.panel-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
/* ── Quota Bar ── */
.quota-grid{display:flex;flex-wrap:wrap;gap:12px 20px;align-items:center}
.quota-bar{flex:1 1 260px;min-width:220px}
.quota-bar .bar-track{height:22px}
.quota-bar .bar-fill{min-width:fit-content}
.quota-pct{font-size:1.1rem;font-weight:700;line-height:1}
.quota-sub{font-size:0.7rem;color:var(--text-dim);margin-top:2px}
.quota-details{flex:1 1 260px;display:grid;grid-template-columns:auto 1fr;gap:4px 12px;font-size:0.8rem}
.quota-label{color:var(--text-dim)}
.quota-value{color:var(--text-bright)}
/* ── Table ── */
.dec-table{width:100%;border-collapse:collapse;font-size:0.75rem}
.dec-table th{text-align:left;padding:4px 6px;border-bottom:1px solid var(--border);color:var(--text-dim);font-weight:600;position:sticky;top:0;background:var(--surface);z-index:1}
.dec-table td{padding:3px 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}
.dec-table tr:hover td{background:var(--surface-hover)}
.badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:0.7rem;font-weight:600}
.badge-route{background:var(--blue-dim);color:var(--blue)}
.badge-dispatch{background:var(--green-dim);color:var(--green)}
.badge-chat{background:var(--purple);color:#e8d0f0}
.badge-local{background:var(--cyan);color:var(--bg)}
.tier-1{color:var(--green)}
.tier-2{color:var(--yellow)}
.tier-3{color:var(--red)}
/* ── Bars ── */
.bars{display:flex;flex-direction:column;gap:4px}
.bar-row{display:flex;align-items:center;gap:8px;font-size:0.75rem}
.bar-label{flex:0 0 180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bar-track{flex:1;height:18px;background:var(--bg);border-radius:3px;position:relative;overflow:hidden}
.bar-fill{height:100%;border-radius:3px;transition:width 0.4s ease;display:flex;align-items:center;padding:0 6px;font-size:0.7rem;color:var(--bg);font-weight:600;min-width:fit-content}
.bar-val{color:var(--text-dim);flex:0 0 100px;text-align:right}
/* ── Chart ── */
.chart-container{position:relative;width:100%;aspect-ratio:1/1;max-height:260px}
.chart-container canvas{width:100%!important;height:100%!important}
/* ── Warnings ── */
.warnings{display:flex;flex-direction:column;gap:6px}
.warning-item{padding:6px 8px;background:var(--yellow-dim);border-left:3px solid var(--yellow);border-radius:0 var(--radius) var(--radius) 0;font-size:0.78rem;line-height:1.4}
.warning-item:first-child{margin-top:2px}
/* ── Category Breakdown ── */
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 12px}
.cat-item{display:flex;align-items:center;gap:6px;font-size:0.78rem}
.cat-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
.cat-name{flex:1;color:var(--text)}
.cat-val{font-weight:600;color:var(--text-bright);font-variant-numeric:tabular-nums}
/* ── Controls ── */
.controls-section{grid-column:1/-1}
.control-group{margin-bottom:10px}
.control-group:last-child{margin-bottom:0}
.control-title{font-size:0.85rem;font-weight:600;color:var(--cyan);margin-bottom:6px;display:flex;align-items:center;gap:6px}
.control-title span{color:var(--text-dim);font-size:0.75rem;font-weight:400}
/* ── Buttons ── */
.btn{display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text);font-family:var(--font);font-size:0.75rem;cursor:pointer;transition:all 0.15s}
.btn:hover{background:var(--surface-hover);border-color:var(--border-bright);color:var(--text-bright)}
.btn:active{transform:scale(0.97)}
.btn:disabled{opacity:0.5;cursor:not-allowed}
.btn-green{border-color:var(--green-dim);color:var(--green)}
.btn-green:hover{background:var(--green-dim);color:var(--bg)}
.btn-red{border-color:var(--red-dim);color:var(--red)}
.btn-red:hover{background:var(--red-dim);color:var(--bg)}
.btn-group{display:flex;flex-wrap:wrap;gap:6px}
/* ── Toggle Switches ── */
.toggle-row{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:0.78rem}
.toggle{position:relative;width:36px;height:20px;flex-shrink:0}
.toggle input{opacity:0;width:36px;height:20px;position:absolute}
.toggle .slider{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--bg);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all 0.2s}
.toggle .slider::before{content:'';position:absolute;width:14px;height:14px;left:2px;top:2px;background:var(--text-dim);border-radius:50%;transition:all 0.2s}
.toggle input:checked+.slider{background:var(--green-dim);border-color:var(--green)}
.toggle input:checked+.slider::before{transform:translateX(16px);background:var(--green)}
.toggle-label{flex:1}
.toggle-value{color:var(--text-dim);font-size:0.7rem;font-variant-numeric:tabular-nums}
.deviation{color:var(--yellow);font-size:0.65rem}
/* ── Config Editor ── */
.config-table{width:100%;border-collapse:collapse;font-size:0.78rem}
.config-table td{padding:4px 6px;border-bottom:1px solid var(--border)}
.config-table td:first-child{color:var(--text-dim);width:200px}
.config-input{background:var(--bg);border:1px solid var(--border);border-radius:3px;padding:3px 8px;color:var(--text-bright);font-family:var(--font);font-size:0.75rem;width:120px}
.config-input:focus{outline:none;border-color:var(--green-dim)}
.config-input[type="checkbox"]{width:16px;height:16px;accent-color:var(--green)}
/* ── Model Availability ── */
.model-table{width:100%;border-collapse:collapse;font-size:0.7rem}
.model-table th{text-align:left;padding:4px 6px;border-bottom:1px solid var(--border);color:var(--text-dim);font-size:0.68rem;font-weight:600}
.model-table td{padding:3px 6px;border-bottom:1px solid var(--border)}
.model-table tr:hover td{background:var(--surface-hover)}
.model-table select{background:var(--bg);border:1px solid var(--border);border-radius:3px;padding:2px 4px;color:var(--text);font-family:var(--font);font-size:0.68rem}
/* ── Status Messages ── */
.toast{position:fixed;bottom:16px;right:16px;padding:8px 16px;border-radius:var(--radius);font-size:0.78rem;z-index:999;opacity:0;transform:translateY(10px);transition:all 0.3s;pointer-events:none}
.toast.show{opacity:1;transform:translateY(0)}
.toast-success{background:var(--green-dim);color:var(--bg);border:1px solid var(--green)}
.toast-error{background:var(--red-dim);color:var(--bg);border:1px solid var(--red)}
.toast-info{background:var(--blue-dim);color:var(--bg);border:1px solid var(--blue)}
/* ── Empty States ── */
.empty{text-align:center;padding:20px;color:var(--text-dim);font-style:italic}
/* ── Responsive ── */
@media(max-width:900px){
.two-col,.three-col,.two-thirds,.half-half{grid-template-columns:1fr}
.bar-label{flex:0 0 100px}
}
@media print{body{background:#fff;color:#000}}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1>admin@router ▸ dashboard</h1>
<span id="sse-status" class="status-dot sse-connecting"></span>
<span id="sse-text" class="meta">connecting…</span>
</div>
<div class="meta" id="generated-at">—</div>
</div>
<div class="dashboard">
<!-- Row: Quota + Model Availability -->
<div class="grid two-col">
<div class="panel" id="panel-quota">
<h2><span class="icon">⚡</span> Quota Meter</h2>
<div id="quota-content"><div class="empty">Loading…</div></div>
</div>
<div class="panel" id="panel-models">
<h2><span class="icon">◈</span> Model Availability</h2>
<div class="panel-scroll" style="max-height:260px">
<table class="model-table"><thead>
<tr><th>model</th><th>provider</th><th>tier</th><th>status</th><th>override</th></tr>
</thead><tbody id="model-tbody"><tr><td colspan="5" style="color:var(--text-dim)">Loading…</td></tr></tbody></table>
</div>
</div>
</div>
<!-- Row: Decisions + PerModel -->
<div class="grid two-thirds">
<div class="panel">
<h2><span class="icon">⟁</span> Recent Decisions <span id="dec-count" style="color:var(--text-dim);font-weight:400;font-size:0.75rem"></span></h2>
<div class="panel-scroll">
<table class="dec-table"><thead>
<tr><th>time</th><th>kind</th><th>category</th><th>tier</th><th>model</th><th>cost</th><th>prof</th></tr>
</thead><tbody id="dec-tbody"></tbody></table>
</div>
</div>
<div class="panel">
<h2><span class="icon">▐</span> Per-Model Usage</h2>
<div id="bars-content" style="display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto;overscroll-behavior:contain">
<div class="empty">Loading…</div>
</div>
</div>
</div>
<!-- Row: Charts + Warnings -->
<div class="grid three-col">
<div class="panel">
<h2><span class="icon">◉</span> Verdict Mix</h2>
<div class="chart-container"><canvas id="verdict-chart"></canvas></div>
<div id="verdict-legend" style="margin-top:6px;font-size:0.72rem;display:flex;flex-wrap:wrap;gap:6px;justify-content:center"></div>
</div>
<div class="panel">
<h2><span class="icon">◆</span> Category Breakdown</h2>
<div id="cat-content" style="display:flex;flex-direction:column;gap:6px;max-height:260px;overflow-y:auto">
<div class="empty">Loading…</div>
</div>
</div>
<div class="panel">
<h2><span class="icon">⚠</span> Warnings</h2>
<div id="warnings-content">
<div class="empty" id="no-warnings" style="display:none">None</div>
</div>
</div>
</div>
<!-- Row: History -->
<div class="grid one-col">
<div class="panel">
<h2>
<span class="icon">◈</span> History
<span style="display:inline-flex;align-items:center;gap:4px;margin-left:8px">
<button class="btn" style="padding:2px 8px;font-size:0.68rem" data-range="1h">1h</button>
<button class="btn active" style="padding:2px 8px;font-size:0.68rem;background:var(--green-dim);color:var(--bg);border-color:var(--green)" data-range="6h">6h</button>
<button class="btn" style="padding:2px 8px;font-size:0.68rem" data-range="24h">24h</button>
<button class="btn" style="padding:2px 8px;font-size:0.68rem" data-range="7d">7d</button>
<button class="btn" style="padding:2px 8px;font-size:0.68rem" data-range="30d">30d</button>
</span>
</h2>
<div class="chart-container" style="max-height:200px;aspect-ratio:auto"><canvas id="history-chart"></canvas></div>
</div>
</div>
<!-- Controls -->
<div class="panel controls-section">
<h2><span class="icon">⚙</span> Controls</h2>
<!-- Operational Triggers -->
<div class="control-group">
<div class="control-title">Operational Triggers <span>— fire-and-forget maintenance jobs</span></div>
<div class="btn-group">
<button class="btn btn-green" onclick="triggerJob('refresh-catalog')">↻ Refresh Catalog</button>
<button class="btn btn-green" onclick="triggerJob('seed-energy')">⚡ Seed Energy</button>
<button class="btn btn-green" onclick="triggerJob('apply-feedback')">✓ Apply Feedback</button>
<button class="btn btn-red" onclick="triggerJob('restart-service')">⟳ Restart Service</button>
</div>
<div id="job-status" style="margin-top:6px;font-size:0.75rem;min-height:1.4em;color:var(--text-dim)"></div>
</div>
<!-- Runtime Toggles -->
<div class="control-group" id="runtime-section">
<div class="control-title">Runtime Knobs <span>— toggle in-memory; no config.yaml write</span></div>
<div id="runtime-list"></div>
</div>
<!-- Config Editor -->
<div class="control-group" id="config-section">
<div class="control-title">Persisted Config <span>— allowlisted keys only; persisted to config.yaml</span></div>
<table class="config-table" id="config-table">
<tbody><tr><td colspan="3" style="color:var(--text-dim);font-style:italic">Loading config…</td></tr></tbody>
</table>
<button class="btn" style="margin-top:6px" onclick="saveAllConfig()">Save All Config</button>
<span id="config-save-status" style="margin-left:8px;font-size:0.72rem"></span>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast" class="toast"></div>
<script>
/* ═══════════════════════════════════════════════════
admin/frontend/index.html — single-file dashboard
API base: relative (works under /admin/)
═══════════════════════════════════════════════════ */
const API = ''; // relative to /admin/
const SSE_URL = '/events/decisions'; // root-level endpoint
const REFRESH_MS = 30000;
let historyRange = '6h';
let decisionCount = 0;
/* ── Toast ── */
function toast(msg, type = 'info') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = `toast toast-${type} show`;
setTimeout(() => { el.classList.remove('show'); }, 4000);
}
/* ── Fetch Wrapper ── */
async function apiFetch(url, opts = {}) {
try {
const resp = await fetch(url, opts);
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
return await resp.json();
} catch (e) {
console.warn('API call failed:', url, e);
return null;
}
}
/* ═══════════════════════════════════════
DATA FETCHING
═══════════════════════════════════════ */
async function loadSnapshot() {
const data = await apiFetch(`${API}api/snapshot`);
if (data) {
renderQuota(data.quota);
renderDecisions(data.recent_decisions);
renderBars(data.per_model);
renderVerdict(data.verdict_mix);
renderWarnings(data.coverage?.warnings || []);
renderCategoryBreakdown(data.recent_decisions);
document.getElementById('generated-at').textContent = data.generated_at ? new Date(data.generated_at).toLocaleString() : '—';
}
const models = await apiFetch(`${API}api/models`);
if (models) renderModels(models);
const runtime = await apiFetch(`${API}api/runtime`);
if (runtime) renderRuntime(runtime);
const config = await apiFetch(`${API}api/config`);
if (config) renderConfig(config);
}
async function loadHistory() {
const data = await apiFetch(`${API}api/history?range=${historyRange}`);
if (data) renderHistory(data);
}
/* ═══════════════════════════════════════
SSE LIVE STREAM
═══════════════════════════════════════ */
let sseConn = null;
function connectSSE() {
try {
if (sseConn) { sseConn.close(); }
sseConn = new EventSource(SSE_URL);
const statusDot = document.getElementById('sse-status');
const statusText = document.getElementById('sse-text');
statusDot.className = 'status-dot sse-connecting';
statusText.textContent = 'connecting…';
sseConn.onopen = () => {
statusDot.className = 'status-dot sse-connected';
statusText.textContent = 'live';
};
sseConn.onmessage = (ev) => {
try {
const data = JSON.parse(ev.data);
addDecision(data);
} catch (_) { /* heartbeat, ignore */ }
};
sseConn.onerror = () => {
statusDot.className = 'status-dot sse-disconnected';
statusText.textContent = 'reconnecting…';
};
} catch (e) {
document.getElementById('sse-status').className = 'status-dot sse-disconnected';
document.getElementById('sse-text').textContent = 'SSE unavailable';
}
}
function addDecision(dec) {
const tbody = document.getElementById('dec-tbody');
if (!tbody || decisionCount >= 100) { loadSnapshot(); return; } // refresh when buffer full
const row = document.createElement('tr');
const badgeClass = {
route: 'badge-route',
dispatch: 'badge-dispatch',
chat: 'badge-chat',
'local-vision': 'badge-local',
passthrough: 'badge-route'
}[dec.kind] || 'badge-route';
const tierClass = `tier-${dec.task_tier || '?'}`;
const modelStr = `${dec.selected_model || '—'} / ${dec.selected_provider || ''}`;
row.innerHTML = `<td>${formatTs(dec.observed_at || '')}</td>
<td><span class="badge ${badgeClass}">${escapeHtml(String(dec.kind || '?'))}</span></td>
<td title="${escapeHtml(dec.task_category || '')}">${escapeHtml(dec.task_category || '—')}</td>
<td class="${tierClass}">${tierClass === 'tier-?' ? '?' : String(dec.task_tier)}</td>
<td title="${escapeHtml(modelStr)}">${escapeHtml(modelStr)}</td>
<td>${dec.est_cost_usd != null ? '$' + Number(dec.est_cost_usd).toFixed(4) : '—'}</td>
<td>${dec.est_proficiency != null ? Number(dec.est_proficiency).toFixed(2) : '—'}</td>`;
tbody.insertBefore(row, tbody.firstChild);
decisionCount++;
}
/* ═══════════════════════════════════════
RENDER FUNCTIONS
═══════════════════════════════════════ */
let verdictChart = null;
let historyChart = null;
function formatTs(ts) {
if (!ts) return '—';
try {
const d = new Date(ts);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
} catch { return String(ts); }
}
function renderQuota(quota) {
const el = document.getElementById('quota-content');
if (!quota || !quota.plan_kwh) {
el.innerHTML = `<div class="empty">No quota plan configured</div>`;
return;
}
const pct = Math.min(quota.metered_fraction_of_plan * 100, 100);
const color = pct > 90 ? 'var(--red)' : pct > 75 ? 'var(--yellow)' : 'var(--green)';
const displayPct = (quota.metered_fraction_of_plan * 100).toFixed(1);
el.innerHTML = `
<div class="quota-grid">
<div class="quota-bar">
<div class="bar-track">
<div class="bar-fill" style="width:${pct}%;background:${color}">${displayPct}%</div>
</div>
<div class="quota-pct" style="color:${color}">${displayPct}%</div>
<div class="quota-sub">${quota.metered_calls_30d} calls (30d)</div>
</div>
<div class="quota-details">
<span class="quota-label">Plan</span><span class="quota-value">${quota.plan_kwh} kWh</span>
<span class="quota-label">Metered (30d)</span><span class="quota-value">${quota.metered_kwh_30d} kWh</span>
<span class="quota-label">Calls (30d)</span><span class="quota-value">${quota.metered_calls_30d}</span>
<span class="quota-label">Resets</span><span class="quota-value">${quota.reset_date}</span>
<span class="quota-label">Note</span><span class="quota-value" style="font-size:0.7rem;color:var(--text-dim)">${escapeHtml(quota.note || '')}</span>
</div>
</div>`;
}
function renderDecisions(decisions) {
const tbody = document.getElementById('dec-tbody');
const countEl = document.getElementById('dec-count');
if (!decisions || !decisions.length) {
tbody.innerHTML = '<tr><td colspan="7" style="color:var(--text-dim);text-align:center;padding:12px">No decisions yet</td></tr>';
countEl.textContent = '(0)';
decisionCount = 0;
return;
}
countEl.textContent = `(${decisions.length})`;
decisionCount = decisions.length;
const html = decisions.slice(0, 50).map(d => {
const badgeClass = { route:'badge-route', dispatch:'badge-dispatch', chat:'badge-chat', 'local-vision':'badge-local', passthrough:'badge-route' }[d.kind] || 'badge-route';
const tierClass = `tier-${d.task_tier || '?'}`;
const modelStr = `${d.selected_model || '—'} / ${d.selected_provider || ''}`;
return `<tr>
<td>${formatTs(d.observed_at)}</td>
<td><span class="badge ${badgeClass}">${escapeHtml(String(d.kind || '?'))}</span></td>
<td title="${escapeHtml(d.task_category || '')}">${escapeHtml(d.task_category || '—')}</td>
<td class="${tierClass}">${tierClass === 'tier-?' ? '?' : String(d.task_tier)}</td>
<td title="${escapeHtml(modelStr)}">${escapeHtml(modelStr)}</td>
<td>${d.est_cost_usd != null ? '$'+Number(d.est_cost_usd).toFixed(4) : '—'}</td>
<td>${d.est_proficiency != null ? Number(d.est_proficiency).toFixed(2) : '—'}</td>
</tr>`;
}).reverse().join('');
tbody.innerHTML = html;
}
function renderBars(perModel) {
const el = document.getElementById('bars-content');
if (!perModel || !perModel.length) {
el.innerHTML = '<div class="empty">No model data yet</div>';
return;
}
const maxCalls = Math.max(...perModel.map(p => p.calls || 0));
const colors = ['var(--green)','var(--blue)','var(--cyan)','var(--orange)','var(--purple)','var(--yellow)','var(--green)'];
const html = perModel.slice().sort((a,b) => (b.calls||0) - (a.calls||0)).slice(0, 12).map((p, i) => {
const pct = maxCalls ? (p.calls / maxCalls * 100) : 0;
const color = colors[i % colors.length];
const label = `${escapeHtml(String(p.model_id))} / ${escapeHtml(String(p.provider))}`;
return `<div class="bar-row">
<span class="bar-label" title="${label}">${label}</span>
<div class="bar-track">
<div class="bar-fill" style="width:${Math.max(pct,2)}%;background:${color}">${p.calls > 10 ? p.calls : ''}</div>
</div>
<span class="bar-val">$${(p.sum_cost_usd || 0).toFixed(4)} · ${(p.sum_energy_kwh || 0).toFixed(5)}kWh</span>
</div>`;
}).join('');
el.innerHTML = html;
}
function renderVerdict(mix) {
const el = document.getElementById('verdict-legend');
if (!mix || !Object.keys(mix).length) {
el.innerHTML = '<span style="color:var(--text-dim)">No verdict data yet</span>';
if (verdictChart) { verdictChart.destroy(); verdictChart = null; }
return;
}
const labels = Object.keys(mix);
const values = labels.map(l => mix[l]);
const colors = [];
const labelsLC = labels.map(l => l.toLowerCase());
const verdictColors = {
pass: 'var(--green)', approve: 'var(--green)', verified: 'var(--green)',
fail: 'var(--red)', reject: 'var(--red)', rejected: 'var(--red)',
ambiguous: 'var(--yellow)', unknown: 'var(--text-dim)'
};
for (const l of labelsLC) {
let found = false;
for (const [k, c] of Object.entries(verdictColors)) {
if (l.includes(k)) { colors.push(c); found = true; break; }
}
if (!found) colors.push(`hsl(${Math.abs(hashCode(l)) % 360}, 60%, 60%)`);
}
if (verdictChart) { updateChart('verdict', labels, values, 'doughnut', colors); }
else { verdictChart = renderChart('verdict-chart', labels, values, 'doughnut', colors); }
el.innerHTML = labels.map((l, i) =>
`<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:${colors[i]};margin-right:4px"></span>${escapeHtml(String(l))}: ${values[i]}</span>`
).join('');
}
function renderWarnings(warnings) {
const el = document.getElementById('warnings-content');
if (!warnings || !warnings.length) {
// Rebuild the placeholder inside the new innerHTML so a stale reference
// is never left pointing at a detached node (prevents null crashes).
el.innerHTML = '<div class="empty" id="no-warnings">None</div>';
return;
}
el.innerHTML = warnings.map(w => `<div class="warning-item">${escapeHtml(w)}</div>`).join('');
}
function renderCategoryBreakdown(decisions) {
const el = document.getElementById('cat-content');
if (!decisions || !decisions.length) { el.innerHTML = '<div class="empty">No decisions</div>'; return; }
const counts = {};
const palette = ['#3498db','#2ecc71','#e74c3c','#f39c12','#9b59b6','#1abc9c','#e67e22','#8e44ad','#16a085','#d35400'];
for (const d of decisions) {
if (d.task_category) counts[d.task_category] = (counts[d.task_category] || 0) + 1;
}
const sorted = Object.entries(counts).sort((a,b) => b[1] - a[1]);
el.innerHTML = sorted.map(([cat, count], i) =>
`<div class="cat-item">
<span class="cat-dot" style="background:${palette[i % palette.length]}"></span>
<span class="cat-name">${escapeHtml(cat)}</span>
<span class="cat-val">${count}</span>
</div>`
).join('');
}
function renderHistory(data) {
const seriesNames = Object.keys(data);
if (!seriesNames.length || seriesNames.every(s => !data[s] || !data[s].length)) {
if (historyChart) { historyChart.destroy(); historyChart = null; return; }
return;
}
const seriesColors = {
decisions_per_bucket: { border: '#3498db', bg: 'rgba(52,152,219,0.1)' },
requests_per_bucket: { border: '#9b59b6', bg: 'rgba(155,89,182,0.1)' },
cost_per_bucket: { border: '#2ecc71', bg: 'rgba(46,204,113,0.1)' },
energy_per_bucket: { border: '#e67e22', bg: 'rgba(230,126,34,0.1)' },
carbon_per_bucket: { border: '#e74c3c', bg: 'rgba(231,76,60,0.1)' },
};
const seriesLabels = {
decisions_per_bucket: 'Decisions',
requests_per_bucket: 'Requests',
cost_per_bucket: 'Cost (USD)',
energy_per_bucket: 'Energy (kWh)',
carbon_per_bucket: 'Carbon (g)',
};
const datasets = seriesNames.map(s => {
const pts = data[s] || [];
const c = seriesColors[s] || { border: 'var(--text-dim)', bg: 'rgba(255,255,255,0.05)' };
return {
label: seriesLabels[s] || s,
data: pts.map(([ts, v]) => ({ x: ts * 1000, y: v })),
borderColor: c.border,
backgroundColor: c.bg,
borderWidth: 1.5,
pointRadius: 0,
fill: false,
tension: 0.2,
yAxisID: 'y',
};
});
if (historyChart) {
historyChart.data.datasets = datasets;
historyChart.update('none');
} else {
historyChart = renderChart('history-chart', [], datasets, 'line');
}
}
/* ═══════════════════════════════════════
MODEL AVAILABILITY TABLE
═══════════════════════════════════════ */
function renderModels(models) {
const tbody = document.getElementById('model-tbody');
if (!models || !models.length) {
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim);text-align:center">No models</td></tr>';
return;
}
const html = models.filter(m => (m.access_level === 'public') || !m.access_level).slice(0, 50).map(m => {
const availClass = { active: 'var(--green)', deprecated: 'var(--red)', stale: 'var(--yellow)' }[m.effective_availability] || 'var(--text-dim)';
const selectOpts = ['active','deprecated','stale'].map(a =>
`<option value="${a}"${m.effective_availability===a?' selected':''}>${a}</option>`
).join('');
return `<tr data-model="${m.model_id}" data-provider="${m.provider}">
<td>${escapeHtml(m.model_id)}</td>
<td>${escapeHtml(m.provider)}</td>
<td>${m.tier || '?'}</td>
<td style="color:${availClass}">${m.effective_availability}</td>
<td><select onchange="updateAvailability('${escapeHtml(m.model_id)}','${escapeHtml(m.provider)}',this.value)"${m.is_overridden?' disabled':''}>${selectOpts}</select></td>
</tr>`;
}).join('');
tbody.innerHTML = html;
}
async function updateAvailability(modelId, provider, avail) {
// Don't post if already that value via dropdown
const resp = await apiFetch(
`${API}api/models/${encodeURIComponent(modelId)}/${encodeURIComponent(provider)}/availability`,
{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ availability: avail }) }
);
if (resp) toast('Availability updated', 'success');
else toast('Update failed', 'error');
}
/* ═══════════════════════════════════════
OPERATIONAL TRIGGERS
═══════════════════════════════════════ */
async function triggerJob(action) {
const statusEl = document.getElementById('job-status');
let url, query = '';
switch(action) {
case 'refresh-catalog': url = `${API}api/refresh-catalog`; break;
case 'seed-energy': url = `${API}api/seed-energy?samples=5`; query = `?samples=5`; break;
case 'apply-feedback': url = `${API}api/apply-feedback?dry_run=true`; break;
case 'restart-service': url = `${API}api/restart-service`; break;
default: return;
}
statusEl.innerHTML = `<span style="color:var(--yellow)">⏳ Running ${action}…</span>`;
const resp = await apiFetch(url, { method:'POST' });
if (resp) {
if (resp.status === 'restarting') {
statusEl.innerHTML = `<span style="color:var(--red)">⟳ Service restarting — page will reload automatically</span>`;
toast('Service restart triggered', 'info');
setTimeout(() => location.reload(), 5000);
} else {
statusEl.innerHTML = `<span style="color:var(--green)">✓ ${action} — ${resp.status || 'done'}</span>`;
toast(`${action} completed`, 'success');
// Reload data after job finishes
setTimeout(() => loadSnapshot(), 1500);
}
} else {
statusEl.innerHTML = `<span style="color:var(--red)">✗ ${action} failed</span>`;
toast(`${action} failed`, 'error');
}
}
/* ═══════════════════════════════════════
RUNTIME TOGGLES
═══════════════════════════════════════ */
function knobDisplayValue(v) {
// Objects like {enabled: bool} print their inner boolean instead of [object Object].
if (v !== null && typeof v === 'object') {
if ('enabled' in v) return String(v.enabled);
// Fall back to JSON for any other object shape (still never "[object Object]").
try { return JSON.stringify(v); } catch (_) { return String(v); }
}
if (typeof v === 'boolean') return String(v);
return String(v);
}
function renderRuntime(state) {
const el = document.getElementById('runtime-list');
const knobs = state || {};
const html = Object.keys(knobs).map(key => {
const knob = knobs[key];
const persisted = knob.persisted;
const runtime = knob.runtime;
const isDiff = JSON.stringify(persisted) !== JSON.stringify(runtime);
const valStr = knobDisplayValue(persisted);
const runtimeStr = knobDisplayValue(runtime);
if (typeof persisted === 'boolean') {
return `<div class="toggle-row">
<label class="toggle">
<input type="checkbox" ${runtime ? 'checked' : ''} onchange="toggleKnob('${key}', this.checked)">
<span class="slider"></span>
</label>
<span class="toggle-label">${key}</span>
<span class="toggle-value">${valStr}</span>
${isDiff ? `<span class="deviation" title="persisted: ${escapeHtml(valStr)}, runtime: ${escapeHtml(runtimeStr)}">⚠ runtime≠file</span>` : ''}
</div>`;
} else if (typeof persisted === 'string') {
return `<div class="toggle-row">
<span style="flex:0 0 160px;font-size:0.75rem">${key}</span>
<input class="config-input" type="text" value="${escapeHtml(runtimeStr)}" onchange="toggleKnob('${key}', this.value)" style="width:80px">
<span class="toggle-value">${valStr}</span>
${isDiff ? `<span class="deviation" title="persisted: ${escapeHtml(valStr)}, runtime: ${escapeHtml(runtimeStr)}">⚠</span>` : ''}
</div>`;
} else {
return `<div class="toggle-row">
<span style="flex:0 0 160px;font-size:0.75rem">${key}</span>
<span class="toggle-value">${escapeHtml(runtimeStr)}${isDiff ? ` (file: ${escapeHtml(valStr)})` : ''}</span>
</div>`;
}
}).join('');
el.innerHTML = html || '<div class="empty">No runtime knobs</div>';
}
async function toggleKnob(knob, value) {
const body = { value };
if (typeof value === 'string') body.value = value;
const resp = await apiFetch(
`${API}api/runtime/${encodeURIComponent(knob)}`,
{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) }
);
if (resp?.ok) toast(`${knob} toggled`, 'success');
else toast(`Failed to toggle ${knob}`, 'error');
}
/* ═══════════════════════════════════════
PERSISTED CONFIG EDITOR
═══════════════════════════════════════ */
function renderConfig(config) {
const tbody = document.getElementById('config-table');
if (!config || !Object.keys(config).length) {
tbody.innerHTML = '<tr><td colspan="3" style="color:var(--text-dim)">No config data</td></tr>';
return;
}
const html = Object.entries(config).map(([key, val]) => {
if (typeof val === 'boolean') {
return `<tr data-key="${escapeHtml(key)}">
<td>${escapeHtml(key)}</td>
<td><input class="config-input" type="checkbox" ${val ? 'checked' : ''} data-config-input></td>
<td style="color:var(--text-dim);font-size:0.7rem">${val ? 'true' : 'false'}</td>
</tr>`;
}
return `<tr data-key="${escapeHtml(key)}">
<td>${escapeHtml(key)}</td>
<td><input class="config-input" type="text" value="${escapeHtml(String(val))}" data-config-input></td>
<td style="color:var(--text-dim);font-size:0.7rem">${escapeHtml(String(val))}</td>
</tr>`;
}).join('');
tbody.innerHTML = html;
}
async function saveAllConfig() {
const inputs = document.querySelectorAll('#config-table tr[data-key]');
const statusEl = document.getElementById('config-save-status');
let ok = 0;
let failed = 0;
for (const row of inputs) {
const key = row.getAttribute('data-key');
const input = row.querySelector('[data-config-input]');
const val = input.type === 'checkbox' ? input.checked : input.value;
let parsed = val;
if (typeof val === 'string') {
const num = Number(val);
if (!isNaN(num) && val.trim() !== '') parsed = num;
}
statusEl.textContent = `saving ${key}…`;
statusEl.style.color = 'var(--text-dim)';
const res = await apiFetch(
`${API}api/config/${encodeURIComponent(key)}`,
{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ value: parsed }) }
);
if (res === null) {
failed += 1;
row.style.color = 'var(--red)';
} else {
ok += 1;
row.style.color = '';
}
}
const allOk = failed === 0;
statusEl.textContent = allOk ? `all saved (${ok}/${inputs.length}) ✓` : `${failed} write(s) failed, ${ok} saved`;
statusEl.style.color = allOk ? 'var(--green)' : 'var(--red)';
if (allOk) toast('Config updated', 'success');
// Reload snapshot to pick up changes
setTimeout(() => loadSnapshot(), 1500);
}
/* ═══════════════════════════════════════
CHART HELPERS
═══════════════════════════════════════ */
function renderChart(canvasId, labels, valuesOrDatasets, type, colors) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
let opts = {
responsive: true,
maintainAspectRatio: true,
plugins: { legend: { display: false } },
};
if (type === 'doughnut') {
const datasets = [{
data: valuesOrDatasets,
backgroundColor: colors || ['#3498db','#2ecc71','#e74c3c','#f1c40f','#9b59b6','#1abc9c'],
borderWidth: 1,
borderColor: 'var(--surface)'
}];
opts.cutout = '60%';
return new Chart(canvas, { type: 'doughnut', data: { labels, datasets }, options: opts });
}
if (type === 'line') {
opts.scales = {
x: { display: true, type: 'time', time: { tooltipFormat: 'HH:mm', unit: 'minute' }, ticks: { maxTicksLimit: 20 } },
y: { display: true, beginAtZero: true, grid: { color: 'rgba(255,255,255,0.04)' } }
};
opts.plugins.legend = { display: true, labels: { color: 'var(--text)', font: { family: 'JetBrains Mono, monospace', size: 10 } } };
return new Chart(canvas, { type: 'line', data: { labels, datasets: valuesOrDatasets }, options: opts });
}
return new Chart(canvas, { type, data: { labels, datasets: [{ data: valuesOrDatasets, backgroundColor: colors || ['#3498db','#2ecc71','#e74c3c','#f1c40f','#9b59b6'] }] }, options: opts });
}
function updateChart(name, labels, values, type, colors) {
if (name !== 'verdict' || !verdictChart) return;
verdictChart.data.labels = labels;
verdictChart.data.datasets[0].data = values;
verdictChart.data.datasets[0].backgroundColor = colors;
verdictChart.update();
}
/* ═══════════════════════════════════════
HISTORY RANGE SELECTOR
═══════════════════════════════════════ */
function setupHistoryRange() {
document.querySelectorAll('.grid.one-col .btn[data-range]').forEach(btn => {
btn.addEventListener('click', () => {
// Remove active from all
document.querySelectorAll('.grid.one-col .btn[data-range]').forEach(b => {
b.classList.remove('active');
b.style.background = '';
b.style.color = '';
b.style.borderColor = '';
});
// Add active to clicked
btn.classList.add('active');
btn.style.background = 'var(--green-dim)';
btn.style.color = 'var(--bg)';
btn.style.borderColor = 'var(--green)';
historyRange = btn.getAttribute('data-range');
loadHistory();
});
});
}
/* ═══════════════════════════════════════
UTILITIES
═══════════════════════════════════════ */
function escapeHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return hash;
}
/* ═══════════════════════════════════════
INIT
═══════════════════════════════════════ */
function init() {
setupHistoryRange();
loadSnapshot();
loadHistory();
connectSSE();
// Auto-refresh
setInterval(() => {
loadSnapshot();
loadHistory();
}, REFRESH_MS);
}
init();
</script>
</body>
</html>

23
admin_schema.sql Normal file
View File

@@ -0,0 +1,23 @@
-- Admin model overrides table.
--
-- Operators mark specific (model_id, provider) rows as deprecated or stale
-- when catalog data is unreliable or when a model fails production verification.
-- The router reads these rows and excludes deprecated models from candidate
-- selection, so an override takes effect on live /route requests without a
-- code change or restart.
--
-- Run with: sqlite3 router.db < admin_schema.sql
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS admin_model_overrides (
model_id TEXT NOT NULL,
provider TEXT NOT NULL,
availability TEXT NOT NULL, -- 'active' | 'deprecated' | 'stale'
reason TEXT,
updated_at TEXT NOT NULL,
PRIMARY KEY (model_id, provider)
);
CREATE INDEX IF NOT EXISTS idx_admin_model_overrides_availability
ON admin_model_overrides (availability);

166
assets/6krrt-logo.svg Normal file
View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 6krrt — Local LLM Model Router
Logo: a cute little wheeled "6" rover, 2 colors + white -->
<svg
viewBox="0 0 256 256"
width="256"
height="256"
version="1.1"
id="svg14"
sodipodi:docname="6krrt-logo.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs14" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="4.5546875"
inkscape:cx="127.89022"
inkscape:cy="128"
inkscape:window-width="2560"
inkscape:window-height="1374"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg14" />
<!-- Body: digit "6" — bulbous circle + sweeping tail (rounded toe) -->
<circle
cx="146"
cy="84"
r="42"
fill="#f59e0b"
id="circle1" />
<path
d="M124 110 Q 90 140 90 176 Q 90 212 122 212 Q 156 212 172 186"
fill="none"
stroke="#f59e0b"
stroke-width="32"
stroke-linecap="round"
id="path1" />
<!-- Eyes (with light ring so they read on dark) -->
<circle
cx="128"
cy="78"
r="9"
fill="#fff"
id="circle2" />
<circle
cx="164"
cy="78"
r="9"
fill="#fff"
id="circle3" />
<circle
cx="128"
cy="78"
r="6"
fill="#1e293b"
id="circle4" />
<circle
cx="164"
cy="78"
r="6"
fill="#1e293b"
id="circle5" />
<!-- Catchlights -->
<circle
cx="131"
cy="74"
r="2.2"
fill="#fff"
id="circle6" />
<circle
cx="167"
cy="74"
r="2.2"
fill="#fff"
id="circle7" />
<!-- Happy smile, centered under the eyes -->
<path
d="M134 100 Q146 112 158 100"
fill="none"
stroke="#fff"
stroke-width="7"
stroke-linecap="round"
id="path7" />
<path
d="M134 100 Q146 112 158 100"
fill="none"
stroke="#1e293b"
stroke-width="3.5"
stroke-linecap="round"
id="path8" />
<!-- Tiny antenna on top of the 6 -->
<line
x1="150.47498"
y1="47.588425"
x2="156.00674"
y2="27.127609"
stroke="#ffffff"
stroke-width="10.7284"
stroke-linecap="round"
id="line8" />
<line
x1="150.47498"
y1="47.588425"
x2="155.08476"
y2="30.53775"
stroke="#1e293b"
stroke-width="6.25824"
stroke-linecap="round"
id="line9" />
<ellipse
cx="157.68477"
cy="-18.584068"
fill="#ffffff"
id="circle9"
transform="matrix(0.96736977,0.25336877,-0.26098631,0.9653425,0,0)"
rx="9.0509033"
ry="8.8314171"
style="stroke-width:1.78809" />
<ellipse
cx="157.68477"
cy="-18.584068"
fill="#1e293b"
id="circle10"
transform="matrix(0.96736977,0.25336877,-0.26098631,0.9653425,0,0)"
rx="5.7925777"
ry="5.6521058"
style="stroke-width:1.78809" />
<!-- Wheels (bottom: tail toe + body belly) -->
<circle
cx="95"
cy="226"
r="14"
fill="#fff"
id="circle11" />
<circle
cx="150"
cy="226"
r="14"
fill="#fff"
id="circle12" />
<circle
cx="95"
cy="226"
r="9"
fill="#1e293b"
id="circle13" />
<circle
cx="150"
cy="226"
r="9"
fill="#1e293b"
id="circle14" />
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,7 @@
[ 386ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8080/favicon.ico:0
[ 30312ms] TypeError: Cannot read properties of null (reading 'style')
at renderWarnings (http://localhost:8080/admin/:561:10)
at loadSnapshot (http://localhost:8080/admin/:333:5)
[ 60328ms] TypeError: Cannot read properties of null (reading 'style')
at renderWarnings (http://localhost:8080/admin/:561:10)
at loadSnapshot (http://localhost:8080/admin/:333:5)

View File

@@ -0,0 +1,34 @@
[ 393ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0
[ 30281ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 60292ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 69443ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Content) @ http://127.0.0.1:8091/admin/api/config/objective.max_energy_per_request:0
[ 69443ms] [WARNING] API call failed: api/config/objective.max_energy_per_request Error: 422 Unprocessable Content
at apiFetch (http://127.0.0.1:8091/admin/:315:25)
at async saveAllConfig (http://127.0.0.1:8091/admin/:790:17) @ http://127.0.0.1:8091/admin/:317
[ 71341ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 80495ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Content) @ http://127.0.0.1:8091/admin/api/config/objective.max_energy_per_request:0
[ 80495ms] [WARNING] API call failed: api/config/objective.max_energy_per_request Error: 422 Unprocessable Content
at apiFetch (http://127.0.0.1:8091/admin/:315:25)
at async saveAllConfig (http://127.0.0.1:8091/admin/:790:17) @ http://127.0.0.1:8091/admin/:317
[ 82423ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 90303ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)

View File

@@ -0,0 +1,66 @@
[ 437ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0
[ 30179ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 60189ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 90163ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 120170ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 150152ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 180153ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 210151ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 240162ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 270185ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 300171ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 330183ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 360171ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
[ 390186ms] Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be destroyed before the canvas with ID 'verdict-chart' can be reused.
at An (https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js:18:89868)
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)

View File

@@ -0,0 +1,133 @@
[ 285ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0
[ 285980ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_RESET @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 285980ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 285981ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_RESET @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 285981ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 285981ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_RESET @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 285982ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 285982ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_RESET @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 285982ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 285983ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_RESET @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 285983ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 285984ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 285984ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 285984ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 285984ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 285985ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 285985ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 285985ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 285986ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 285986ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 285986ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 285987ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 285987ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 285987ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 285987ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 285988ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 285988ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 285989ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 285989ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 285989ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 285989ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 285990ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 285990ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 285990ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 285990ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 285991ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 285991ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 285992ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 285992ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 285993ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 285993ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 285993ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 285993ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 285994ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 285994ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 285994ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 285994ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 285995ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 285995ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 285995ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 285995ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 300072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 300072ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 300073ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 300073ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 300074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 300074ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 300076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 300076ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 300078ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 300078ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317

View File

@@ -0,0 +1,164 @@
[ 388ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0
[ 220977ms] [ERROR] Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING @ http://127.0.0.1:8091/events/decisions:0
[ 223978ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 226980ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 229982ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 232984ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 235986ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 238987ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 240093ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 240093ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 240094ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 240094ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 240095ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 240095ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 240096ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 240096ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 240098ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 240098ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 241989ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 244991ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 247993ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 250995ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 253997ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 256999ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 260001ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 263003ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 266005ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 269006ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 270092ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 270092ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 270093ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 270093ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 270094ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 270094ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 270095ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 270095ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 270097ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 270097ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 272008ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 275010ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 278012ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 281013ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 284016ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 287018ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 290020ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 293022ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 296024ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 299026ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 300092ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 300093ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 300093ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 300093ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 300094ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 300095ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 300096ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 300096ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 300098ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 300098ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 302028ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 305031ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 308033ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 311035ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 314037ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 317040ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 320042ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 323043ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 326045ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 329047ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 330093ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 330093ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 330094ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 330094ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 330095ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 330095ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 330097ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 330097ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 330099ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 330099ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 332049ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 335051ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 338052ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 341054ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 344057ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 347058ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 350060ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 353062ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 356064ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 359065ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 360093ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 360093ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 360094ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 360094ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 360094ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 360094ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 360096ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 360096ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 360098ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 360098ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 362067ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 365070ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 368071ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 371073ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 374076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0
[ 377078ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/events/decisions:0

View File

@@ -0,0 +1 @@
[ 391ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0

View File

@@ -0,0 +1,66 @@
[ 270047ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 270047ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 270048ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 270048ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 270050ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 270050ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 270052ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 270052ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 270053ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 270053ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 300044ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 300044ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 300045ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 300045ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 300046ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 300046ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 300048ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 300048ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 300050ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 300050ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317
[ 330044ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/snapshot:0
[ 330044ms] [WARNING] API call failed: api/snapshot TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:328:22)
at http://127.0.0.1:8091/admin/:908:5 @ http://127.0.0.1:8091/admin/:317
[ 330045ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/history?range=6h:0
[ 330045ms] [WARNING] API call failed: api/history?range=6h TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadHistory (http://127.0.0.1:8091/admin/:347:22)
at http://127.0.0.1:8091/admin/:909:5 @ http://127.0.0.1:8091/admin/:317
[ 330046ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/models:0
[ 330046ms] [WARNING] API call failed: api/models TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:338:24) @ http://127.0.0.1:8091/admin/:317
[ 330048ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/runtime:0
[ 330048ms] [WARNING] API call failed: api/runtime TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:340:25) @ http://127.0.0.1:8091/admin/:317
[ 330050ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8091/admin/api/config:0
[ 330050ms] [WARNING] API call failed: api/config TypeError: Failed to fetch
at apiFetch (http://127.0.0.1:8091/admin/:314:24)
at loadSnapshot (http://127.0.0.1:8091/admin/:342:24) @ http://127.0.0.1:8091/admin/:317

View File

@@ -0,0 +1,756 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 2:11:18 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e14]:
- generic [ref=e17]:
- generic [ref=e18]: 51.7%
- generic [ref=e19]: 7745 calls
- generic [ref=e20]:
- generic [ref=e21]: Plan
- generic [ref=e22]: 6.25 kWh
- generic [ref=e23]: Metered (30d)
- generic [ref=e24]: 3.22931 kWh
- generic [ref=e25]: Calls (30d)
- generic [ref=e26]: "7745"
- generic [ref=e27]: Resets
- generic [ref=e28]: 2026-07-30
- generic [ref=e29]: Note
- generic [ref=e30]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e31]:
- heading "◈ Model Availability" [level=2] [ref=e32]:
- generic [ref=e33]: ◈
- text: Model Availability
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "model" [ref=e38]
- columnheader "provider" [ref=e39]
- columnheader "tier" [ref=e40]
- columnheader "status" [ref=e41]
- columnheader "override" [ref=e42]
- rowgroup [ref=e43]:
- row [ref=e44]:
- cell "deepseek-v4-flash" [ref=e45]
- cell "neuralwatt" [ref=e46]
- cell "2" [ref=e47]
- cell "active" [ref=e48]
- cell "active" [ref=e49]:
- combobox [ref=e50]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e51]:
- cell "deepseek-v4-flash-flex" [ref=e52]
- cell "neuralwatt" [ref=e53]
- cell "2" [ref=e54]
- cell "active" [ref=e55]
- cell "active" [ref=e56]:
- combobox [ref=e57]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e58]:
- cell "gemma-4-31b" [ref=e59]
- cell "neuralwatt" [ref=e60]
- cell "1" [ref=e61]
- cell "active" [ref=e62]
- cell "active" [ref=e63]:
- combobox [ref=e64]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e65]:
- cell "glm-5.2-fast" [ref=e66]
- cell "neuralwatt" [ref=e67]
- cell "2" [ref=e68]
- cell "active" [ref=e69]
- cell "active" [ref=e70]:
- combobox [ref=e71]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e72]:
- cell "glm-5.2-flex" [ref=e73]
- cell "neuralwatt" [ref=e74]
- cell "3" [ref=e75]
- cell "active" [ref=e76]
- cell "active" [ref=e77]:
- combobox [ref=e78]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e79]:
- cell "glm-5.3" [ref=e80]
- cell "neuralwatt" [ref=e81]
- cell "3" [ref=e82]
- cell "active" [ref=e83]
- cell "active" [ref=e84]:
- combobox [ref=e85]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e86]:
- cell "kimi-k2.7-code" [ref=e87]
- cell "neuralwatt" [ref=e88]
- cell "3" [ref=e89]
- cell "active" [ref=e90]
- cell "active" [ref=e91]:
- combobox [ref=e92]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e93]:
- cell "kimi-k2.7-code-fast" [ref=e94]
- cell "neuralwatt" [ref=e95]
- cell "2" [ref=e96]
- cell "active" [ref=e97]
- cell "active" [ref=e98]:
- combobox [ref=e99]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e100]:
- cell "kimi-k2.7-code-flex" [ref=e101]
- cell "neuralwatt" [ref=e102]
- cell "3" [ref=e103]
- cell "active" [ref=e104]
- cell "active" [ref=e105]:
- combobox [ref=e106]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e107]:
- cell "kimi-k3" [ref=e108]
- cell "neuralwatt" [ref=e109]
- cell "3" [ref=e110]
- cell "active" [ref=e111]
- cell "active" [ref=e112]:
- combobox [ref=e113]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e114]:
- cell "kimi-k3-fast" [ref=e115]
- cell "neuralwatt" [ref=e116]
- cell "2" [ref=e117]
- cell "active" [ref=e118]
- cell "active" [ref=e119]:
- combobox [ref=e120]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e121]:
- cell "kimi-k3-flex" [ref=e122]
- cell "neuralwatt" [ref=e123]
- cell "3" [ref=e124]
- cell "active" [ref=e125]
- cell "active" [ref=e126]:
- combobox [ref=e127]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e128]:
- cell "qwen3.6-35b" [ref=e129]
- cell "neuralwatt" [ref=e130]
- cell "3" [ref=e131]
- cell "active" [ref=e132]
- cell "active" [ref=e133]:
- combobox [ref=e134]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e135]:
- cell "qwen3.6-35b-fast" [ref=e136]
- cell "neuralwatt" [ref=e137]
- cell "2" [ref=e138]
- cell "active" [ref=e139]
- cell "active" [ref=e140]:
- combobox [ref=e141]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- generic [ref=e142]:
- generic [ref=e143]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e144]:
- generic [ref=e145]: ⟁
- text: Recent Decisions
- generic [ref=e146]: (50)
- table [ref=e148]:
- rowgroup [ref=e149]:
- row [ref=e150]:
- columnheader "time" [ref=e151]
- columnheader "kind" [ref=e152]
- columnheader "category" [ref=e153]
- columnheader "tier" [ref=e154]
- columnheader "model" [ref=e155]
- columnheader "cost" [ref=e156]
- columnheader "prof" [ref=e157]
- rowgroup [ref=e158]:
- row [ref=e159]:
- cell "02:10:28 AM" [ref=e160]
- cell "chat" [ref=e161]
- cell "tool_use_agentic" [ref=e163]
- cell "2" [ref=e164]
- cell "qwen3.6-35b / neuralwatt" [ref=e165]
- cell "$0.0043" [ref=e166]
- cell "1.00" [ref=e167]
- row [ref=e168]:
- cell "02:10:13 AM" [ref=e169]
- cell "chat" [ref=e170]
- cell "tool_use_agentic" [ref=e172]
- cell "2" [ref=e173]
- cell "qwen3.6-35b / neuralwatt" [ref=e174]
- cell "$0.0043" [ref=e175]
- cell "1.00" [ref=e176]
- row [ref=e177]:
- cell "02:10:06 AM" [ref=e178]
- cell "chat" [ref=e179]
- cell "tool_use_agentic" [ref=e181]
- cell "2" [ref=e182]
- cell "qwen3.6-35b / neuralwatt" [ref=e183]
- cell "$0.0042" [ref=e184]
- cell "1.00" [ref=e185]
- row [ref=e186]:
- cell "02:09:56 AM" [ref=e187]
- cell "chat" [ref=e188]
- cell "tool_use_agentic" [ref=e190]
- cell "2" [ref=e191]
- cell "qwen3.6-35b / neuralwatt" [ref=e192]
- cell "$0.0042" [ref=e193]
- cell "1.00" [ref=e194]
- row [ref=e195]:
- cell "01:59:55 AM" [ref=e196]
- cell "chat" [ref=e197]
- cell "coding_refactor" [ref=e199]
- cell "3" [ref=e200]
- cell "qwen3.6-35b / neuralwatt" [ref=e201]
- cell "$0.0052" [ref=e202]
- cell "1.00" [ref=e203]
- row [ref=e204]:
- cell "02:00:00 AM" [ref=e205]
- cell "chat" [ref=e206]
- cell "coding_refactor" [ref=e208]
- cell "3" [ref=e209]
- cell "qwen3.6-35b / neuralwatt" [ref=e210]
- cell "$0.0053" [ref=e211]
- cell "1.00" [ref=e212]
- row [ref=e213]:
- cell "02:00:12 AM" [ref=e214]
- cell "chat" [ref=e215]
- cell "tool_use_agentic" [ref=e217]
- cell "2" [ref=e218]
- cell "qwen3.6-35b / neuralwatt" [ref=e219]
- cell "$0.0040" [ref=e220]
- cell "1.00" [ref=e221]
- row [ref=e222]:
- cell "02:00:13 AM" [ref=e223]
- cell "chat" [ref=e224]
- cell "coding_general" [ref=e226]
- cell "3" [ref=e227]
- cell "qwen3.6-35b / neuralwatt" [ref=e228]
- cell "$0.0053" [ref=e229]
- cell "1.00" [ref=e230]
- row [ref=e231]:
- cell "02:00:23 AM" [ref=e232]
- cell "chat" [ref=e233]
- cell "tool_use_agentic" [ref=e235]
- cell "2" [ref=e236]
- cell "qwen3.6-35b / neuralwatt" [ref=e237]
- cell "$0.0040" [ref=e238]
- cell "1.00" [ref=e239]
- row [ref=e240]:
- cell "02:00:24 AM" [ref=e241]
- cell "chat" [ref=e242]
- cell "coding_general" [ref=e244]
- cell "3" [ref=e245]
- cell "qwen3.6-35b / neuralwatt" [ref=e246]
- cell "$0.0053" [ref=e247]
- cell "1.00" [ref=e248]
- row [ref=e249]:
- cell "02:00:28 AM" [ref=e250]
- cell "chat" [ref=e251]
- cell "tool_use_agentic" [ref=e253]
- cell "2" [ref=e254]
- cell "qwen3.6-35b / neuralwatt" [ref=e255]
- cell "$0.0041" [ref=e256]
- cell "1.00" [ref=e257]
- row [ref=e258]:
- cell "02:01:33 AM" [ref=e259]
- cell "chat" [ref=e260]
- cell "coding_general" [ref=e262]
- cell "3" [ref=e263]
- cell "qwen3.6-35b / neuralwatt" [ref=e264]
- cell "$0.0053" [ref=e265]
- cell "1.00" [ref=e266]
- row [ref=e267]:
- cell "02:01:38 AM" [ref=e268]
- cell "chat" [ref=e269]
- cell "tool_use_agentic" [ref=e271]
- cell "2" [ref=e272]
- cell "qwen3.6-35b / neuralwatt" [ref=e273]
- cell "$0.0041" [ref=e274]
- cell "1.00" [ref=e275]
- row [ref=e276]:
- cell "02:01:39 AM" [ref=e277]
- cell "chat" [ref=e278]
- cell "coding_refactor" [ref=e280]
- cell "3" [ref=e281]
- cell "qwen3.6-35b / neuralwatt" [ref=e282]
- cell "$0.0053" [ref=e283]
- cell "1.00" [ref=e284]
- row [ref=e285]:
- cell "02:01:49 AM" [ref=e286]
- cell "chat" [ref=e287]
- cell "coding_refactor" [ref=e289]
- cell "3" [ref=e290]
- cell "qwen3.6-35b / neuralwatt" [ref=e291]
- cell "$0.0053" [ref=e292]
- cell "1.00" [ref=e293]
- row [ref=e294]:
- cell "02:01:54 AM" [ref=e295]
- cell "chat" [ref=e296]
- cell "coding_refactor" [ref=e298]
- cell "3" [ref=e299]
- cell "kimi-k2.7-code / neuralwatt" [ref=e300]
- cell "$0.0181" [ref=e301]
- cell "1.00" [ref=e302]
- row [ref=e303]:
- cell "02:02:08 AM" [ref=e304]
- cell "chat" [ref=e305]
- cell "coding_general" [ref=e307]
- cell "3" [ref=e308]
- cell "kimi-k2.7-code / neuralwatt" [ref=e309]
- cell "$0.0182" [ref=e310]
- cell "1.00" [ref=e311]
- row [ref=e312]:
- cell "02:02:51 AM" [ref=e313]
- cell "chat" [ref=e314]
- cell "coding_refactor" [ref=e316]
- cell "2" [ref=e317]
- cell "deepseek-v4-flash / neuralwatt" [ref=e318]
- cell "$0.0038" [ref=e319]
- cell "1.00" [ref=e320]
- row [ref=e321]:
- cell "02:02:59 AM" [ref=e322]
- cell "chat" [ref=e323]
- cell "coding_general" [ref=e325]
- cell "2" [ref=e326]
- cell "deepseek-v4-flash / neuralwatt" [ref=e327]
- cell "$0.0038" [ref=e328]
- cell "1.00" [ref=e329]
- row [ref=e330]:
- cell "02:03:13 AM" [ref=e331]
- cell "chat" [ref=e332]
- cell "general_chat" [ref=e334]
- cell "1" [ref=e335]
- cell "gemma-4-31b / neuralwatt" [ref=e336]
- cell "$0.0044" [ref=e337]
- cell "1.00" [ref=e338]
- row [ref=e339]:
- cell "02:03:46 AM" [ref=e340]
- cell "chat" [ref=e341]
- cell "reasoning_math" [ref=e343]
- cell "2" [ref=e344]
- cell "qwen3.6-35b / neuralwatt" [ref=e345]
- cell "$0.0032" [ref=e346]
- cell "1.00" [ref=e347]
- row [ref=e348]:
- cell "02:03:52 AM" [ref=e349]
- cell "chat" [ref=e350]
- cell "reasoning_math" [ref=e352]
- cell "2" [ref=e353]
- cell "qwen3.6-35b / neuralwatt" [ref=e354]
- cell "$0.0033" [ref=e355]
- cell "1.00" [ref=e356]
- row [ref=e357]:
- cell "02:03:56 AM" [ref=e358]
- cell "chat" [ref=e359]
- cell "coding_refactor" [ref=e361]
- cell "3" [ref=e362]
- cell "qwen3.6-35b / neuralwatt" [ref=e363]
- cell "$0.0032" [ref=e364]
- cell "1.00" [ref=e365]
- row [ref=e366]:
- cell "02:03:57 AM" [ref=e367]
- cell "chat" [ref=e368]
- cell "reasoning_math" [ref=e370]
- cell "2" [ref=e371]
- cell "qwen3.6-35b / neuralwatt" [ref=e372]
- cell "$0.0033" [ref=e373]
- cell "1.00" [ref=e374]
- row [ref=e375]:
- cell "02:04:00 AM" [ref=e376]
- cell "chat" [ref=e377]
- cell "coding_refactor" [ref=e379]
- cell "3" [ref=e380]
- cell "qwen3.6-35b / neuralwatt" [ref=e381]
- cell "$0.0033" [ref=e382]
- cell "1.00" [ref=e383]
- row [ref=e384]:
- cell "02:04:02 AM" [ref=e385]
- cell "chat" [ref=e386]
- cell "reasoning_math" [ref=e388]
- cell "2" [ref=e389]
- cell "qwen3.6-35b / neuralwatt" [ref=e390]
- cell "$0.0047" [ref=e391]
- cell "1.00" [ref=e392]
- row [ref=e393]:
- cell "02:04:05 AM" [ref=e394]
- cell "chat" [ref=e395]
- cell "coding_refactor" [ref=e397]
- cell "3" [ref=e398]
- cell "qwen3.6-35b / neuralwatt" [ref=e399]
- cell "$0.0033" [ref=e400]
- cell "1.00" [ref=e401]
- row [ref=e402]:
- cell "02:04:06 AM" [ref=e403]
- cell "chat" [ref=e404]
- cell "tool_use_agentic" [ref=e406]
- cell "3" [ref=e407]
- cell "qwen3.6-35b / neuralwatt" [ref=e408]
- cell "$0.0032" [ref=e409]
- cell "1.00" [ref=e410]
- row [ref=e411]:
- cell "02:04:08 AM" [ref=e412]
- cell "chat" [ref=e413]
- cell "reasoning_math" [ref=e415]
- cell "2" [ref=e416]
- cell "qwen3.6-35b / neuralwatt" [ref=e417]
- cell "$0.0047" [ref=e418]
- cell "1.00" [ref=e419]
- row [ref=e420]:
- cell "02:04:10 AM" [ref=e421]
- cell "chat" [ref=e422]
- cell "coding_refactor" [ref=e424]
- cell "3" [ref=e425]
- cell "qwen3.6-35b / neuralwatt" [ref=e426]
- cell "$0.0051" [ref=e427]
- cell "1.00" [ref=e428]
- row [ref=e429]:
- cell "02:04:12 AM" [ref=e430]
- cell "chat" [ref=e431]
- cell "tool_use_agentic" [ref=e433]
- cell "2" [ref=e434]
- cell "qwen3.6-35b / neuralwatt" [ref=e435]
- cell "$0.0033" [ref=e436]
- cell "1.00" [ref=e437]
- row [ref=e438]:
- cell "02:04:16 AM" [ref=e439]
- cell "chat" [ref=e440]
- cell "debugging" [ref=e442]
- cell "2" [ref=e443]
- cell "deepseek-v4-flash / neuralwatt" [ref=e444]
- cell "$0.0021" [ref=e445]
- cell "1.00" [ref=e446]
- row [ref=e447]:
- cell "02:04:18 AM" [ref=e448]
- cell "chat" [ref=e449]
- cell "coding_refactor" [ref=e451]
- cell "3" [ref=e452]
- cell "kimi-k2.7-code / neuralwatt" [ref=e453]
- cell "$0.0199" [ref=e454]
- cell "1.00" [ref=e455]
- row [ref=e456]:
- cell "02:04:19 AM" [ref=e457]
- cell "chat" [ref=e458]
- cell "reasoning_math" [ref=e460]
- cell "2" [ref=e461]
- cell "qwen3.6-35b / neuralwatt" [ref=e462]
- cell "$0.0048" [ref=e463]
- cell "1.00" [ref=e464]
- row [ref=e465]:
- cell "02:04:21 AM" [ref=e466]
- cell "chat" [ref=e467]
- cell "tool_use_agentic" [ref=e469]
- cell "2" [ref=e470]
- cell "qwen3.6-35b / neuralwatt" [ref=e471]
- cell "$0.0042" [ref=e472]
- cell "1.00" [ref=e473]
- row [ref=e474]:
- cell "02:04:23 AM" [ref=e475]
- cell "chat" [ref=e476]
- cell "tool_use_agentic" [ref=e478]
- cell "2" [ref=e479]
- cell "qwen3.6-35b / neuralwatt" [ref=e480]
- cell "$0.0033" [ref=e481]
- cell "1.00" [ref=e482]
- row [ref=e483]:
- cell "02:04:25 AM" [ref=e484]
- cell "chat" [ref=e485]
- cell "reasoning_math" [ref=e487]
- cell "2" [ref=e488]
- cell "kimi-k2.7-code / neuralwatt" [ref=e489]
- cell "$0.0180" [ref=e490]
- cell "1.00" [ref=e491]
- row [ref=e492]:
- cell "02:04:27 AM" [ref=e493]
- cell "chat" [ref=e494]
- cell "coding_refactor" [ref=e496]
- cell "3" [ref=e497]
- cell "kimi-k2.7-code / neuralwatt" [ref=e498]
- cell "$0.0212" [ref=e499]
- cell "1.00" [ref=e500]
- row [ref=e501]:
- cell "02:04:28 AM" [ref=e502]
- cell "chat" [ref=e503]
- cell "tool_use_agentic" [ref=e505]
- cell "2" [ref=e506]
- cell "qwen3.6-35b / neuralwatt" [ref=e507]
- cell "$0.0042" [ref=e508]
- cell "1.00" [ref=e509]
- row [ref=e510]:
- cell "02:04:30 AM" [ref=e511]
- cell "chat" [ref=e512]
- cell "reasoning_math" [ref=e514]
- cell "2" [ref=e515]
- cell "qwen3.6-35b / neuralwatt" [ref=e516]
- cell "$0.0039" [ref=e517]
- cell "1.00" [ref=e518]
- row [ref=e519]:
- cell "02:04:33 AM" [ref=e520]
- cell "chat" [ref=e521]
- cell "reasoning_math" [ref=e523]
- cell "2" [ref=e524]
- cell "kimi-k2.7-code / neuralwatt" [ref=e525]
- cell "$0.0198" [ref=e526]
- cell "1.00" [ref=e527]
- row [ref=e528]:
- cell "02:04:34 AM" [ref=e529]
- cell "chat" [ref=e530]
- cell "tool_use_agentic" [ref=e532]
- cell "2" [ref=e533]
- cell "qwen3.6-35b / neuralwatt" [ref=e534]
- cell "$0.0042" [ref=e535]
- cell "1.00" [ref=e536]
- row [ref=e537]:
- cell "02:04:36 AM" [ref=e538]
- cell "chat" [ref=e539]
- cell "coding_refactor" [ref=e541]
- cell "3" [ref=e542]
- cell "kimi-k2.7-code / neuralwatt" [ref=e543]
- cell "$0.0212" [ref=e544]
- cell "1.00" [ref=e545]
- row [ref=e546]:
- cell "02:04:38 AM" [ref=e547]
- cell "chat" [ref=e548]
- cell "reasoning_math" [ref=e550]
- cell "2" [ref=e551]
- cell "qwen3.6-35b / neuralwatt" [ref=e552]
- cell "$0.0040" [ref=e553]
- cell "1.00" [ref=e554]
- row [ref=e555]:
- cell "02:04:40 AM" [ref=e556]
- cell "chat" [ref=e557]
- cell "reasoning_math" [ref=e559]
- cell "2" [ref=e560]
- cell "kimi-k2.7-code / neuralwatt" [ref=e561]
- cell "$0.0200" [ref=e562]
- cell "1.00" [ref=e563]
- row [ref=e564]:
- cell "02:04:41 AM" [ref=e565]
- cell "chat" [ref=e566]
- cell "tool_use_agentic" [ref=e568]
- cell "2" [ref=e569]
- cell "qwen3.6-35b / neuralwatt" [ref=e570]
- cell "$0.0042" [ref=e571]
- cell "1.00" [ref=e572]
- row [ref=e573]:
- cell "02:04:43 AM" [ref=e574]
- cell "chat" [ref=e575]
- cell "coding_refactor" [ref=e577]
- cell "3" [ref=e578]
- cell "kimi-k2.7-code / neuralwatt" [ref=e579]
- cell "$0.0251" [ref=e580]
- cell "1.00" [ref=e581]
- row [ref=e582]:
- cell "02:04:45 AM" [ref=e583]
- cell "chat" [ref=e584]
- cell "tool_use_agentic" [ref=e586]
- cell "2" [ref=e587]
- cell "qwen3.6-35b / neuralwatt" [ref=e588]
- cell "$0.0040" [ref=e589]
- cell "1.00" [ref=e590]
- row [ref=e591]:
- cell "02:04:47 AM" [ref=e592]
- cell "chat" [ref=e593]
- cell "reasoning_math" [ref=e595]
- cell "2" [ref=e596]
- cell "kimi-k2.7-code / neuralwatt" [ref=e597]
- cell "$0.0201" [ref=e598]
- cell "1.00" [ref=e599]
- row [ref=e600]:
- cell "02:06:45 AM" [ref=e601]
- cell "chat" [ref=e602]
- cell "tool_use_agentic" [ref=e604]
- cell "2" [ref=e605]
- cell "qwen3.6-35b / neuralwatt" [ref=e606]
- cell "$0.0042" [ref=e607]
- cell "1.00" [ref=e608]
- row [ref=e609]:
- cell "02:09:56 AM" [ref=e610]
- cell "chat" [ref=e611]
- cell "tool_use_agentic" [ref=e613]
- cell "2" [ref=e614]
- cell "qwen3.6-35b / neuralwatt" [ref=e615]
- cell "$0.0042" [ref=e616]
- cell "1.00" [ref=e617]
- row [ref=e618]:
- cell "02:10:06 AM" [ref=e619]
- cell "chat" [ref=e620]
- cell "tool_use_agentic" [ref=e622]
- cell "2" [ref=e623]
- cell "qwen3.6-35b / neuralwatt" [ref=e624]
- cell "$0.0042" [ref=e625]
- cell "1.00" [ref=e626]
- row [ref=e627]:
- cell "02:10:13 AM" [ref=e628]
- cell "chat" [ref=e629]
- cell "tool_use_agentic" [ref=e631]
- cell "2" [ref=e632]
- cell "qwen3.6-35b / neuralwatt" [ref=e633]
- cell "$0.0043" [ref=e634]
- cell "1.00" [ref=e635]
- row [ref=e636]:
- cell "02:10:28 AM" [ref=e637]
- cell "chat" [ref=e638]
- cell "tool_use_agentic" [ref=e640]
- cell "2" [ref=e641]
- cell "qwen3.6-35b / neuralwatt" [ref=e642]
- cell "$0.0043" [ref=e643]
- cell "1.00" [ref=e644]
- generic [ref=e645]:
- heading "▐ Per-Model Usage" [level=2] [ref=e646]:
- generic [ref=e647]: ▐
- text: Per-Model Usage
- generic [ref=e648]:
- generic [ref=e649]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e650]
- generic [ref=e651]: "4152"
- generic [ref=e653]: $8.1409 · 1.07746kWh
- generic [ref=e654]:
- generic "qwen3.6-35b / neuralwatt" [ref=e655]
- generic [ref=e656]: "1271"
- generic [ref=e658]: $1.6557 · 0.20701kWh
- generic [ref=e659]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e660]
- generic [ref=e661]: "833"
- generic [ref=e663]: $4.9976 · 0.62497kWh
- generic [ref=e664]:
- generic "glm-5.2-fast / neuralwatt" [ref=e665]
- generic [ref=e666]: "355"
- generic [ref=e668]: $4.9416 · 0.61835kWh
- generic [ref=e669]:
- generic "gemma-4-31b / neuralwatt" [ref=e670]
- generic [ref=e671]: "352"
- generic [ref=e673]: $1.0384 · 0.15188kWh
- generic [ref=e674]:
- generic "glm-5.2-flex / neuralwatt" [ref=e675]
- generic [ref=e676]: "127"
- generic [ref=e678]: $0.6401 · 0.12080kWh
- generic [ref=e679]:
- generic "kimi-k3 / neuralwatt" [ref=e680]
- generic [ref=e681]: "127"
- generic [ref=e683]: $2.4135 · 0.30169kWh
- generic [ref=e684]:
- generic "kimi-k3-fast / neuralwatt" [ref=e685]
- generic [ref=e686]: "96"
- generic [ref=e688]: $0.5209 · 0.06511kWh
- generic [ref=e689]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e690]
- generic [ref=e691]: "85"
- generic [ref=e693]: $0.0129 · 0.00324kWh
- generic [ref=e694]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e695]
- generic [ref=e696]: "85"
- generic [ref=e698]: $0.0779 · 0.00974kWh
- generic [ref=e699]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e700]
- generic [ref=e701]: "85"
- generic [ref=e703]: $0.0903 · 0.01129kWh
- generic [ref=e704]:
- generic "kimi-k3-flex / neuralwatt" [ref=e705]
- generic [ref=e706]: "85"
- generic [ref=e708]: $0.2678 · 0.03348kWh
- generic [ref=e709]:
- generic [ref=e710]:
- heading "◉ Verdict Mix" [level=2] [ref=e711]:
- generic [ref=e712]: ◉
- text: Verdict Mix
- generic [ref=e715]:
- generic [ref=e716]: "failed: 114"
- generic [ref=e718]: "malformed: 50"
- generic [ref=e720]: "ok: 164"
- generic [ref=e722]: "succeeded: 509"
- generic [ref=e724]: "truncated: 9"
- generic [ref=e726]: "unverifiable: 6420"
- generic [ref=e728]:
- heading "◆ Category Breakdown" [level=2] [ref=e729]:
- generic [ref=e730]: ◆
- text: Category Breakdown
- generic [ref=e731]:
- generic [ref=e732]:
- generic [ref=e734]: tool_use_agentic
- generic [ref=e735]: "17"
- generic [ref=e736]:
- generic [ref=e738]: coding_refactor
- generic [ref=e739]: "14"
- generic [ref=e740]:
- generic [ref=e742]: reasoning_math
- generic [ref=e743]: "12"
- generic [ref=e744]:
- generic [ref=e746]: coding_general
- generic [ref=e747]: "5"
- generic [ref=e748]:
- generic [ref=e750]: debugging
- generic [ref=e751]: "1"
- generic [ref=e752]:
- generic [ref=e754]: general_chat
- generic [ref=e755]: "1"
- generic [ref=e756]:
- heading "⚠ Warnings" [level=2] [ref=e757]:
- generic [ref=e758]: ⚠
- text: Warnings
- generic [ref=e759]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e763]:
- generic [ref=e764]: ◈
- text: History
- generic [ref=e765]:
- button "1h" [ref=e766] [cursor=pointer]
- button "6h" [ref=e767] [cursor=pointer]
- button "24h" [ref=e768] [cursor=pointer]
- button "7d" [ref=e769] [cursor=pointer]
- button "30d" [ref=e770] [cursor=pointer]
- generic [ref=e773]:
- heading "⚙ Controls" [level=2] [ref=e774]:
- generic [ref=e775]: ⚙
- text: Controls
- generic [ref=e776]:
- generic [ref=e777]:
- text: Operational Triggers
- generic [ref=e778]: — fire-and-forget maintenance jobs
- generic [ref=e779]:
- button "↻ Refresh Catalog" [ref=e780] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e781] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e782] [cursor=pointer]
- button "⟳ Restart Service" [ref=e783] [cursor=pointer]
- generic [ref=e786]:
- text: Runtime Knobs
- generic [ref=e787]: — toggle in-memory; no config.yaml write
- generic [ref=e788]:
- generic [ref=e789]:
- text: Persisted Config
- generic [ref=e790]: — allowlisted keys only; persisted to config.yaml
- table [ref=e791]:
- rowgroup [ref=e792]:
- row [ref=e793]:
- cell "Loading config…" [ref=e794]
- button "Save All Config" [ref=e795] [cursor=pointer]

View File

@@ -0,0 +1,851 @@
- generic [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 2:11:18 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e14]:
- generic [ref=e17]:
- generic [ref=e18]: 51.7%
- generic [ref=e19]: 7745 calls
- generic [ref=e20]:
- generic [ref=e21]: Plan
- generic [ref=e22]: 6.25 kWh
- generic [ref=e23]: Metered (30d)
- generic [ref=e24]: 3.22931 kWh
- generic [ref=e25]: Calls (30d)
- generic [ref=e26]: "7745"
- generic [ref=e27]: Resets
- generic [ref=e28]: 2026-07-30
- generic [ref=e29]: Note
- generic [ref=e30]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e31]:
- heading "◈ Model Availability" [level=2] [ref=e32]:
- generic [ref=e33]: ◈
- text: Model Availability
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "model" [ref=e38]
- columnheader "provider" [ref=e39]
- columnheader "tier" [ref=e40]
- columnheader "status" [ref=e41]
- columnheader "override" [ref=e42]
- rowgroup [ref=e43]:
- row [ref=e44]:
- cell "deepseek-v4-flash" [ref=e45]
- cell "neuralwatt" [ref=e46]
- cell "2" [ref=e47]
- cell "active" [ref=e48]
- cell "active" [ref=e49]:
- combobox [ref=e50]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e51]:
- cell "deepseek-v4-flash-flex" [ref=e52]
- cell "neuralwatt" [ref=e53]
- cell "2" [ref=e54]
- cell "active" [ref=e55]
- cell "active" [ref=e56]:
- combobox [ref=e57]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e58]:
- cell "gemma-4-31b" [ref=e59]
- cell "neuralwatt" [ref=e60]
- cell "1" [ref=e61]
- cell "active" [ref=e62]
- cell "active" [ref=e63]:
- combobox [ref=e64]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e65]:
- cell "glm-5.2-fast" [ref=e66]
- cell "neuralwatt" [ref=e67]
- cell "2" [ref=e68]
- cell "active" [ref=e69]
- cell "active" [ref=e70]:
- combobox [ref=e71]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e72]:
- cell "glm-5.2-flex" [ref=e73]
- cell "neuralwatt" [ref=e74]
- cell "3" [ref=e75]
- cell "active" [ref=e76]
- cell "active" [ref=e77]:
- combobox [ref=e78]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e79]:
- cell "glm-5.3" [ref=e80]
- cell "neuralwatt" [ref=e81]
- cell "3" [ref=e82]
- cell "active" [ref=e83]
- cell "active" [ref=e84]:
- combobox [ref=e85]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e86]:
- cell "kimi-k2.7-code" [ref=e87]
- cell "neuralwatt" [ref=e88]
- cell "3" [ref=e89]
- cell "active" [ref=e90]
- cell "active" [ref=e91]:
- combobox [ref=e92]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e93]:
- cell "kimi-k2.7-code-fast" [ref=e94]
- cell "neuralwatt" [ref=e95]
- cell "2" [ref=e96]
- cell "active" [ref=e97]
- cell "active" [ref=e98]:
- combobox [ref=e99]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e100]:
- cell "kimi-k2.7-code-flex" [ref=e101]
- cell "neuralwatt" [ref=e102]
- cell "3" [ref=e103]
- cell "active" [ref=e104]
- cell "active" [ref=e105]:
- combobox [ref=e106]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e107]:
- cell "kimi-k3" [ref=e108]
- cell "neuralwatt" [ref=e109]
- cell "3" [ref=e110]
- cell "active" [ref=e111]
- cell "active" [ref=e112]:
- combobox [ref=e113]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e114]:
- cell "kimi-k3-fast" [ref=e115]
- cell "neuralwatt" [ref=e116]
- cell "2" [ref=e117]
- cell "active" [ref=e118]
- cell "active" [ref=e119]:
- combobox [ref=e120]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e121]:
- cell "kimi-k3-flex" [ref=e122]
- cell "neuralwatt" [ref=e123]
- cell "3" [ref=e124]
- cell "active" [ref=e125]
- cell "active" [ref=e126]:
- combobox [ref=e127]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e128]:
- cell "qwen3.6-35b" [ref=e129]
- cell "neuralwatt" [ref=e130]
- cell "3" [ref=e131]
- cell "active" [ref=e132]
- cell "active" [ref=e133]:
- combobox [ref=e134]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e135]:
- cell "qwen3.6-35b-fast" [ref=e136]
- cell "neuralwatt" [ref=e137]
- cell "2" [ref=e138]
- cell "active" [ref=e139]
- cell "active" [ref=e140]:
- combobox [ref=e141]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- generic [ref=e142]:
- generic [ref=e143]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e144]:
- generic [ref=e145]: ⟁
- text: Recent Decisions
- generic [ref=e146]: (50)
- table [ref=e148]:
- rowgroup [ref=e149]:
- row [ref=e150]:
- columnheader "time" [ref=e151]
- columnheader "kind" [ref=e152]
- columnheader "category" [ref=e153]
- columnheader "tier" [ref=e154]
- columnheader "model" [ref=e155]
- columnheader "cost" [ref=e156]
- columnheader "prof" [ref=e157]
- rowgroup [ref=e158]:
- row [ref=e159]:
- cell "02:10:28 AM" [ref=e160]
- cell "chat" [ref=e161]
- cell "tool_use_agentic" [ref=e163]
- cell "2" [ref=e164]
- cell "qwen3.6-35b / neuralwatt" [ref=e165]
- cell "$0.0043" [ref=e166]
- cell "1.00" [ref=e167]
- row [ref=e168]:
- cell "02:10:13 AM" [ref=e169]
- cell "chat" [ref=e170]
- cell "tool_use_agentic" [ref=e172]
- cell "2" [ref=e173]
- cell "qwen3.6-35b / neuralwatt" [ref=e174]
- cell "$0.0043" [ref=e175]
- cell "1.00" [ref=e176]
- row [ref=e177]:
- cell "02:10:06 AM" [ref=e178]
- cell "chat" [ref=e179]
- cell "tool_use_agentic" [ref=e181]
- cell "2" [ref=e182]
- cell "qwen3.6-35b / neuralwatt" [ref=e183]
- cell "$0.0042" [ref=e184]
- cell "1.00" [ref=e185]
- row [ref=e186]:
- cell "02:09:56 AM" [ref=e187]
- cell "chat" [ref=e188]
- cell "tool_use_agentic" [ref=e190]
- cell "2" [ref=e191]
- cell "qwen3.6-35b / neuralwatt" [ref=e192]
- cell "$0.0042" [ref=e193]
- cell "1.00" [ref=e194]
- row [ref=e195]:
- cell "01:59:55 AM" [ref=e196]
- cell "chat" [ref=e197]
- cell "coding_refactor" [ref=e199]
- cell "3" [ref=e200]
- cell "qwen3.6-35b / neuralwatt" [ref=e201]
- cell "$0.0052" [ref=e202]
- cell "1.00" [ref=e203]
- row [ref=e204]:
- cell "02:00:00 AM" [ref=e205]
- cell "chat" [ref=e206]
- cell "coding_refactor" [ref=e208]
- cell "3" [ref=e209]
- cell "qwen3.6-35b / neuralwatt" [ref=e210]
- cell "$0.0053" [ref=e211]
- cell "1.00" [ref=e212]
- row [ref=e213]:
- cell "02:00:12 AM" [ref=e214]
- cell "chat" [ref=e215]
- cell "tool_use_agentic" [ref=e217]
- cell "2" [ref=e218]
- cell "qwen3.6-35b / neuralwatt" [ref=e219]
- cell "$0.0040" [ref=e220]
- cell "1.00" [ref=e221]
- row [ref=e222]:
- cell "02:00:13 AM" [ref=e223]
- cell "chat" [ref=e224]
- cell "coding_general" [ref=e226]
- cell "3" [ref=e227]
- cell "qwen3.6-35b / neuralwatt" [ref=e228]
- cell "$0.0053" [ref=e229]
- cell "1.00" [ref=e230]
- row [ref=e231]:
- cell "02:00:23 AM" [ref=e232]
- cell "chat" [ref=e233]
- cell "tool_use_agentic" [ref=e235]
- cell "2" [ref=e236]
- cell "qwen3.6-35b / neuralwatt" [ref=e237]
- cell "$0.0040" [ref=e238]
- cell "1.00" [ref=e239]
- row [ref=e240]:
- cell "02:00:24 AM" [ref=e241]
- cell "chat" [ref=e242]
- cell "coding_general" [ref=e244]
- cell "3" [ref=e245]
- cell "qwen3.6-35b / neuralwatt" [ref=e246]
- cell "$0.0053" [ref=e247]
- cell "1.00" [ref=e248]
- row [ref=e249]:
- cell "02:00:28 AM" [ref=e250]
- cell "chat" [ref=e251]
- cell "tool_use_agentic" [ref=e253]
- cell "2" [ref=e254]
- cell "qwen3.6-35b / neuralwatt" [ref=e255]
- cell "$0.0041" [ref=e256]
- cell "1.00" [ref=e257]
- row [ref=e258]:
- cell "02:01:33 AM" [ref=e259]
- cell "chat" [ref=e260]
- cell "coding_general" [ref=e262]
- cell "3" [ref=e263]
- cell "qwen3.6-35b / neuralwatt" [ref=e264]
- cell "$0.0053" [ref=e265]
- cell "1.00" [ref=e266]
- row [ref=e267]:
- cell "02:01:38 AM" [ref=e268]
- cell "chat" [ref=e269]
- cell "tool_use_agentic" [ref=e271]
- cell "2" [ref=e272]
- cell "qwen3.6-35b / neuralwatt" [ref=e273]
- cell "$0.0041" [ref=e274]
- cell "1.00" [ref=e275]
- row [ref=e276]:
- cell "02:01:39 AM" [ref=e277]
- cell "chat" [ref=e278]
- cell "coding_refactor" [ref=e280]
- cell "3" [ref=e281]
- cell "qwen3.6-35b / neuralwatt" [ref=e282]
- cell "$0.0053" [ref=e283]
- cell "1.00" [ref=e284]
- row [ref=e285]:
- cell "02:01:49 AM" [ref=e286]
- cell "chat" [ref=e287]
- cell "coding_refactor" [ref=e289]
- cell "3" [ref=e290]
- cell "qwen3.6-35b / neuralwatt" [ref=e291]
- cell "$0.0053" [ref=e292]
- cell "1.00" [ref=e293]
- row [ref=e294]:
- cell "02:01:54 AM" [ref=e295]
- cell "chat" [ref=e296]
- cell "coding_refactor" [ref=e298]
- cell "3" [ref=e299]
- cell "kimi-k2.7-code / neuralwatt" [ref=e300]
- cell "$0.0181" [ref=e301]
- cell "1.00" [ref=e302]
- row [ref=e303]:
- cell "02:02:08 AM" [ref=e304]
- cell "chat" [ref=e305]
- cell "coding_general" [ref=e307]
- cell "3" [ref=e308]
- cell "kimi-k2.7-code / neuralwatt" [ref=e309]
- cell "$0.0182" [ref=e310]
- cell "1.00" [ref=e311]
- row [ref=e312]:
- cell "02:02:51 AM" [ref=e313]
- cell "chat" [ref=e314]
- cell "coding_refactor" [ref=e316]
- cell "2" [ref=e317]
- cell "deepseek-v4-flash / neuralwatt" [ref=e318]
- cell "$0.0038" [ref=e319]
- cell "1.00" [ref=e320]
- row [ref=e321]:
- cell "02:02:59 AM" [ref=e322]
- cell "chat" [ref=e323]
- cell "coding_general" [ref=e325]
- cell "2" [ref=e326]
- cell "deepseek-v4-flash / neuralwatt" [ref=e327]
- cell "$0.0038" [ref=e328]
- cell "1.00" [ref=e329]
- row [ref=e330]:
- cell "02:03:13 AM" [ref=e331]
- cell "chat" [ref=e332]
- cell "general_chat" [ref=e334]
- cell "1" [ref=e335]
- cell "gemma-4-31b / neuralwatt" [ref=e336]
- cell "$0.0044" [ref=e337]
- cell "1.00" [ref=e338]
- row [ref=e339]:
- cell "02:03:46 AM" [ref=e340]
- cell "chat" [ref=e341]
- cell "reasoning_math" [ref=e343]
- cell "2" [ref=e344]
- cell "qwen3.6-35b / neuralwatt" [ref=e345]
- cell "$0.0032" [ref=e346]
- cell "1.00" [ref=e347]
- row [ref=e348]:
- cell "02:03:52 AM" [ref=e349]
- cell "chat" [ref=e350]
- cell "reasoning_math" [ref=e352]
- cell "2" [ref=e353]
- cell "qwen3.6-35b / neuralwatt" [ref=e354]
- cell "$0.0033" [ref=e355]
- cell "1.00" [ref=e356]
- row [ref=e357]:
- cell "02:03:56 AM" [ref=e358]
- cell "chat" [ref=e359]
- cell "coding_refactor" [ref=e361]
- cell "3" [ref=e362]
- cell "qwen3.6-35b / neuralwatt" [ref=e363]
- cell "$0.0032" [ref=e364]
- cell "1.00" [ref=e365]
- row [ref=e366]:
- cell "02:03:57 AM" [ref=e367]
- cell "chat" [ref=e368]
- cell "reasoning_math" [ref=e370]
- cell "2" [ref=e371]
- cell "qwen3.6-35b / neuralwatt" [ref=e372]
- cell "$0.0033" [ref=e373]
- cell "1.00" [ref=e374]
- row [ref=e375]:
- cell "02:04:00 AM" [ref=e376]
- cell "chat" [ref=e377]
- cell "coding_refactor" [ref=e379]
- cell "3" [ref=e380]
- cell "qwen3.6-35b / neuralwatt" [ref=e381]
- cell "$0.0033" [ref=e382]
- cell "1.00" [ref=e383]
- row [ref=e384]:
- cell "02:04:02 AM" [ref=e385]
- cell "chat" [ref=e386]
- cell "reasoning_math" [ref=e388]
- cell "2" [ref=e389]
- cell "qwen3.6-35b / neuralwatt" [ref=e390]
- cell "$0.0047" [ref=e391]
- cell "1.00" [ref=e392]
- row [ref=e393]:
- cell "02:04:05 AM" [ref=e394]
- cell "chat" [ref=e395]
- cell "coding_refactor" [ref=e397]
- cell "3" [ref=e398]
- cell "qwen3.6-35b / neuralwatt" [ref=e399]
- cell "$0.0033" [ref=e400]
- cell "1.00" [ref=e401]
- row [ref=e402]:
- cell "02:04:06 AM" [ref=e403]
- cell "chat" [ref=e404]
- cell "tool_use_agentic" [ref=e406]
- cell "3" [ref=e407]
- cell "qwen3.6-35b / neuralwatt" [ref=e408]
- cell "$0.0032" [ref=e409]
- cell "1.00" [ref=e410]
- row [ref=e411]:
- cell "02:04:08 AM" [ref=e412]
- cell "chat" [ref=e413]
- cell "reasoning_math" [ref=e415]
- cell "2" [ref=e416]
- cell "qwen3.6-35b / neuralwatt" [ref=e417]
- cell "$0.0047" [ref=e418]
- cell "1.00" [ref=e419]
- row [ref=e420]:
- cell "02:04:10 AM" [ref=e421]
- cell "chat" [ref=e422]
- cell "coding_refactor" [ref=e424]
- cell "3" [ref=e425]
- cell "qwen3.6-35b / neuralwatt" [ref=e426]
- cell "$0.0051" [ref=e427]
- cell "1.00" [ref=e428]
- row [ref=e429]:
- cell "02:04:12 AM" [ref=e430]
- cell "chat" [ref=e431]
- cell "tool_use_agentic" [ref=e433]
- cell "2" [ref=e434]
- cell "qwen3.6-35b / neuralwatt" [ref=e435]
- cell "$0.0033" [ref=e436]
- cell "1.00" [ref=e437]
- row [ref=e438]:
- cell "02:04:16 AM" [ref=e439]
- cell "chat" [ref=e440]
- cell "debugging" [ref=e442]
- cell "2" [ref=e443]
- cell "deepseek-v4-flash / neuralwatt" [ref=e444]
- cell "$0.0021" [ref=e445]
- cell "1.00" [ref=e446]
- row [ref=e447]:
- cell "02:04:18 AM" [ref=e448]
- cell "chat" [ref=e449]
- cell "coding_refactor" [ref=e451]
- cell "3" [ref=e452]
- cell "kimi-k2.7-code / neuralwatt" [ref=e453]
- cell "$0.0199" [ref=e454]
- cell "1.00" [ref=e455]
- row [ref=e456]:
- cell "02:04:19 AM" [ref=e457]
- cell "chat" [ref=e458]
- cell "reasoning_math" [ref=e460]
- cell "2" [ref=e461]
- cell "qwen3.6-35b / neuralwatt" [ref=e462]
- cell "$0.0048" [ref=e463]
- cell "1.00" [ref=e464]
- row [ref=e465]:
- cell "02:04:21 AM" [ref=e466]
- cell "chat" [ref=e467]
- cell "tool_use_agentic" [ref=e469]
- cell "2" [ref=e470]
- cell "qwen3.6-35b / neuralwatt" [ref=e471]
- cell "$0.0042" [ref=e472]
- cell "1.00" [ref=e473]
- row [ref=e474]:
- cell "02:04:23 AM" [ref=e475]
- cell "chat" [ref=e476]
- cell "tool_use_agentic" [ref=e478]
- cell "2" [ref=e479]
- cell "qwen3.6-35b / neuralwatt" [ref=e480]
- cell "$0.0033" [ref=e481]
- cell "1.00" [ref=e482]
- row [ref=e483]:
- cell "02:04:25 AM" [ref=e484]
- cell "chat" [ref=e485]
- cell "reasoning_math" [ref=e487]
- cell "2" [ref=e488]
- cell "kimi-k2.7-code / neuralwatt" [ref=e489]
- cell "$0.0180" [ref=e490]
- cell "1.00" [ref=e491]
- row [ref=e492]:
- cell "02:04:27 AM" [ref=e493]
- cell "chat" [ref=e494]
- cell "coding_refactor" [ref=e496]
- cell "3" [ref=e497]
- cell "kimi-k2.7-code / neuralwatt" [ref=e498]
- cell "$0.0212" [ref=e499]
- cell "1.00" [ref=e500]
- row [ref=e501]:
- cell "02:04:28 AM" [ref=e502]
- cell "chat" [ref=e503]
- cell "tool_use_agentic" [ref=e505]
- cell "2" [ref=e506]
- cell "qwen3.6-35b / neuralwatt" [ref=e507]
- cell "$0.0042" [ref=e508]
- cell "1.00" [ref=e509]
- row [ref=e510]:
- cell "02:04:30 AM" [ref=e511]
- cell "chat" [ref=e512]
- cell "reasoning_math" [ref=e514]
- cell "2" [ref=e515]
- cell "qwen3.6-35b / neuralwatt" [ref=e516]
- cell "$0.0039" [ref=e517]
- cell "1.00" [ref=e518]
- row [ref=e519]:
- cell "02:04:33 AM" [ref=e520]
- cell "chat" [ref=e521]
- cell "reasoning_math" [ref=e523]
- cell "2" [ref=e524]
- cell "kimi-k2.7-code / neuralwatt" [ref=e525]
- cell "$0.0198" [ref=e526]
- cell "1.00" [ref=e527]
- row [ref=e528]:
- cell "02:04:34 AM" [ref=e529]
- cell "chat" [ref=e530]
- cell "tool_use_agentic" [ref=e532]
- cell "2" [ref=e533]
- cell "qwen3.6-35b / neuralwatt" [ref=e534]
- cell "$0.0042" [ref=e535]
- cell "1.00" [ref=e536]
- row [ref=e537]:
- cell "02:04:36 AM" [ref=e538]
- cell "chat" [ref=e539]
- cell "coding_refactor" [ref=e541]
- cell "3" [ref=e542]
- cell "kimi-k2.7-code / neuralwatt" [ref=e543]
- cell "$0.0212" [ref=e544]
- cell "1.00" [ref=e545]
- row [ref=e546]:
- cell "02:04:38 AM" [ref=e547]
- cell "chat" [ref=e548]
- cell "reasoning_math" [ref=e550]
- cell "2" [ref=e551]
- cell "qwen3.6-35b / neuralwatt" [ref=e552]
- cell "$0.0040" [ref=e553]
- cell "1.00" [ref=e554]
- row [ref=e555]:
- cell "02:04:40 AM" [ref=e556]
- cell "chat" [ref=e557]
- cell "reasoning_math" [ref=e559]
- cell "2" [ref=e560]
- cell "kimi-k2.7-code / neuralwatt" [ref=e561]
- cell "$0.0200" [ref=e562]
- cell "1.00" [ref=e563]
- row [ref=e564]:
- cell "02:04:41 AM" [ref=e565]
- cell "chat" [ref=e566]
- cell "tool_use_agentic" [ref=e568]
- cell "2" [ref=e569]
- cell "qwen3.6-35b / neuralwatt" [ref=e570]
- cell "$0.0042" [ref=e571]
- cell "1.00" [ref=e572]
- row [ref=e573]:
- cell "02:04:43 AM" [ref=e574]
- cell "chat" [ref=e575]
- cell "coding_refactor" [ref=e577]
- cell "3" [ref=e578]
- cell "kimi-k2.7-code / neuralwatt" [ref=e579]
- cell "$0.0251" [ref=e580]
- cell "1.00" [ref=e581]
- row [ref=e582]:
- cell "02:04:45 AM" [ref=e583]
- cell "chat" [ref=e584]
- cell "tool_use_agentic" [ref=e586]
- cell "2" [ref=e587]
- cell "qwen3.6-35b / neuralwatt" [ref=e588]
- cell "$0.0040" [ref=e589]
- cell "1.00" [ref=e590]
- row [ref=e591]:
- cell "02:04:47 AM" [ref=e592]
- cell "chat" [ref=e593]
- cell "reasoning_math" [ref=e595]
- cell "2" [ref=e596]
- cell "kimi-k2.7-code / neuralwatt" [ref=e597]
- cell "$0.0201" [ref=e598]
- cell "1.00" [ref=e599]
- row [ref=e600]:
- cell "02:06:45 AM" [ref=e601]
- cell "chat" [ref=e602]
- cell "tool_use_agentic" [ref=e604]
- cell "2" [ref=e605]
- cell "qwen3.6-35b / neuralwatt" [ref=e606]
- cell "$0.0042" [ref=e607]
- cell "1.00" [ref=e608]
- row [ref=e609]:
- cell "02:09:56 AM" [ref=e610]
- cell "chat" [ref=e611]
- cell "tool_use_agentic" [ref=e613]
- cell "2" [ref=e614]
- cell "qwen3.6-35b / neuralwatt" [ref=e615]
- cell "$0.0042" [ref=e616]
- cell "1.00" [ref=e617]
- row [ref=e618]:
- cell "02:10:06 AM" [ref=e619]
- cell "chat" [ref=e620]
- cell "tool_use_agentic" [ref=e622]
- cell "2" [ref=e623]
- cell "qwen3.6-35b / neuralwatt" [ref=e624]
- cell "$0.0042" [ref=e625]
- cell "1.00" [ref=e626]
- row [ref=e627]:
- cell "02:10:13 AM" [ref=e628]
- cell "chat" [ref=e629]
- cell "tool_use_agentic" [ref=e631]
- cell "2" [ref=e632]
- cell "qwen3.6-35b / neuralwatt" [ref=e633]
- cell "$0.0043" [ref=e634]
- cell "1.00" [ref=e635]
- row [ref=e636]:
- cell "02:10:28 AM" [ref=e637]
- cell "chat" [ref=e638]
- cell "tool_use_agentic" [ref=e640]
- cell "2" [ref=e641]
- cell "qwen3.6-35b / neuralwatt" [ref=e642]
- cell "$0.0043" [ref=e643]
- cell "1.00" [ref=e644]
- generic [ref=e645]:
- heading "▐ Per-Model Usage" [level=2] [ref=e646]:
- generic [ref=e647]: ▐
- text: Per-Model Usage
- generic [ref=e648]:
- generic [ref=e649]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e650]
- generic [ref=e651]: "4152"
- generic [ref=e653]: $8.1409 · 1.07746kWh
- generic [ref=e654]:
- generic "qwen3.6-35b / neuralwatt" [ref=e655]
- generic [ref=e656]: "1271"
- generic [ref=e658]: $1.6557 · 0.20701kWh
- generic [ref=e659]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e660]
- generic [ref=e661]: "833"
- generic [ref=e663]: $4.9976 · 0.62497kWh
- generic [ref=e664]:
- generic "glm-5.2-fast / neuralwatt" [ref=e665]
- generic [ref=e666]: "355"
- generic [ref=e668]: $4.9416 · 0.61835kWh
- generic [ref=e669]:
- generic "gemma-4-31b / neuralwatt" [ref=e670]
- generic [ref=e671]: "352"
- generic [ref=e673]: $1.0384 · 0.15188kWh
- generic [ref=e674]:
- generic "glm-5.2-flex / neuralwatt" [ref=e675]
- generic [ref=e676]: "127"
- generic [ref=e678]: $0.6401 · 0.12080kWh
- generic [ref=e679]:
- generic "kimi-k3 / neuralwatt" [ref=e680]
- generic [ref=e681]: "127"
- generic [ref=e683]: $2.4135 · 0.30169kWh
- generic [ref=e684]:
- generic "kimi-k3-fast / neuralwatt" [ref=e685]
- generic [ref=e686]: "96"
- generic [ref=e688]: $0.5209 · 0.06511kWh
- generic [ref=e689]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e690]
- generic [ref=e691]: "85"
- generic [ref=e693]: $0.0129 · 0.00324kWh
- generic [ref=e694]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e695]
- generic [ref=e696]: "85"
- generic [ref=e698]: $0.0779 · 0.00974kWh
- generic [ref=e699]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e700]
- generic [ref=e701]: "85"
- generic [ref=e703]: $0.0903 · 0.01129kWh
- generic [ref=e704]:
- generic "kimi-k3-flex / neuralwatt" [ref=e705]
- generic [ref=e706]: "85"
- generic [ref=e708]: $0.2678 · 0.03348kWh
- generic [ref=e709]:
- generic [ref=e710]:
- heading "◉ Verdict Mix" [level=2] [ref=e711]:
- generic [ref=e712]: ◉
- text: Verdict Mix
- generic [ref=e715]:
- generic [ref=e716]: "failed: 114"
- generic [ref=e718]: "malformed: 50"
- generic [ref=e720]: "ok: 164"
- generic [ref=e722]: "succeeded: 509"
- generic [ref=e724]: "truncated: 9"
- generic [ref=e726]: "unverifiable: 6420"
- generic [ref=e728]:
- heading "◆ Category Breakdown" [level=2] [ref=e729]:
- generic [ref=e730]: ◆
- text: Category Breakdown
- generic [ref=e731]:
- generic [ref=e732]:
- generic [ref=e734]: tool_use_agentic
- generic [ref=e735]: "17"
- generic [ref=e736]:
- generic [ref=e738]: coding_refactor
- generic [ref=e739]: "14"
- generic [ref=e740]:
- generic [ref=e742]: reasoning_math
- generic [ref=e743]: "12"
- generic [ref=e744]:
- generic [ref=e746]: coding_general
- generic [ref=e747]: "5"
- generic [ref=e748]:
- generic [ref=e750]: debugging
- generic [ref=e751]: "1"
- generic [ref=e752]:
- generic [ref=e754]: general_chat
- generic [ref=e755]: "1"
- generic [ref=e756]:
- heading "⚠ Warnings" [level=2] [ref=e757]:
- generic [ref=e758]: ⚠
- text: Warnings
- generic [ref=e759]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e763]:
- generic [ref=e764]: ◈
- text: History
- generic [ref=e765]:
- button "1h" [ref=e766] [cursor=pointer]
- button "6h" [ref=e767] [cursor=pointer]
- button "24h" [ref=e768] [cursor=pointer]
- button "7d" [ref=e769] [cursor=pointer]
- button "30d" [ref=e770] [cursor=pointer]
- generic [ref=e773]:
- heading "⚙ Controls" [level=2] [ref=e774]:
- generic [ref=e775]: ⚙
- text: Controls
- generic [ref=e776]:
- generic [ref=e777]:
- text: Operational Triggers
- generic [ref=e778]: — fire-and-forget maintenance jobs
- generic [ref=e779]:
- button "↻ Refresh Catalog" [ref=e780] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e781] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e782] [cursor=pointer]
- button "⟳ Restart Service" [ref=e783] [cursor=pointer]
- generic [ref=e785]:
- generic [ref=e786]:
- text: Runtime Knobs
- generic [ref=e787]: — toggle in-memory; no config.yaml write
- generic [ref=e796]:
- generic [ref=e797]:
- generic [ref=e798]:
- checkbox [checked] [active] [ref=e799]
- generic [ref=e800] [cursor=pointer]
- generic [ref=e801]: log_route_decisions
- generic [ref=e802]: "true"
- 'generic "persisted: true, runtime: false" [ref=e803]': ⚠ runtime≠file
- generic [ref=e804]:
- generic [ref=e805]:
- checkbox [checked] [ref=e806]
- generic [ref=e807] [cursor=pointer]
- generic [ref=e808]: log_energy_observations
- generic [ref=e809]: "true"
- generic [ref=e810]:
- generic [ref=e811]: circuit_breaker
- generic [ref=e812]: "[object Object]"
- generic [ref=e813]:
- generic [ref=e814]:
- checkbox [checked] [ref=e815]
- generic [ref=e816] [cursor=pointer]
- generic [ref=e817]: local_llm_enabled
- generic [ref=e818]: "true"
- generic [ref=e819]:
- generic [ref=e820]:
- checkbox [ref=e821]
- generic [ref=e822] [cursor=pointer]
- generic [ref=e823]: session_cache_enabled
- generic [ref=e824]: "false"
- generic [ref=e825]:
- generic [ref=e826]:
- checkbox [ref=e827]
- generic [ref=e828] [cursor=pointer]
- generic [ref=e829]: pinch_enabled
- generic [ref=e830]: "false"
- generic [ref=e831]:
- generic [ref=e832]:
- checkbox [ref=e833]
- generic [ref=e834] [cursor=pointer]
- generic [ref=e835]: pinch_relevance_enabled
- generic [ref=e836]: "false"
- generic [ref=e837]:
- generic [ref=e838]: default_flex_preference
- textbox [ref=e839]: auto
- generic [ref=e840]: auto
- generic [ref=e788]:
- generic [ref=e789]:
- text: Persisted Config
- generic [ref=e790]: — allowlisted keys only; persisted to config.yaml
- table [ref=e791]:
- rowgroup [ref=e841]:
- row [ref=e842]:
- cell "logging.level" [ref=e843]
- cell [ref=e844]:
- textbox [ref=e845]: info
- cell "info" [ref=e846]
- row [ref=e847]:
- cell "objective.quality_tolerance" [ref=e848]
- cell [ref=e849]:
- textbox [ref=e850]: "0.1"
- cell "0.1" [ref=e851]
- row [ref=e852]:
- cell "objective.max_energy_per_request" [ref=e853]
- cell [ref=e854]:
- textbox [ref=e855]: "null"
- cell "null" [ref=e856]
- row [ref=e857]:
- cell "objective.plan_kwh_per_period" [ref=e858]
- cell [ref=e859]:
- textbox [ref=e860]: "6.25"
- cell "6.25" [ref=e861]
- row [ref=e862]:
- cell "circuit_breaker.enabled" [ref=e863]
- cell [ref=e864]:
- checkbox [ref=e865]
- cell "false" [ref=e866]
- row [ref=e867]:
- cell "session_cache.enabled" [ref=e868]
- cell [ref=e869]:
- checkbox [ref=e870]
- cell "false" [ref=e871]
- row [ref=e872]:
- cell "verification.local_llm_enabled" [ref=e873]
- cell [ref=e874]:
- checkbox [checked] [ref=e875]
- cell "true" [ref=e876]
- row [ref=e877]:
- cell "pinch.enabled" [ref=e878]
- cell [ref=e879]:
- checkbox [ref=e880]
- cell "false" [ref=e881]
- row [ref=e882]:
- cell "pinch.relevance.enabled" [ref=e883]
- cell [ref=e884]:
- checkbox [ref=e885]
- cell "false" [ref=e886]
- row [ref=e887]:
- cell "routing.default_flex_preference" [ref=e888]
- cell [ref=e889]:
- textbox [ref=e890]: auto
- cell "auto" [ref=e891]
- button "Save All Config" [ref=e795] [cursor=pointer]
- generic: log_route_decisions toggled

View File

@@ -0,0 +1,95 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: —
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e13]: Loading…
- generic [ref=e15]:
- heading "◈ Model Availability" [level=2] [ref=e16]:
- generic [ref=e17]: ◈
- text: Model Availability
- table [ref=e19]:
- rowgroup [ref=e20]:
- row [ref=e21]:
- columnheader "model" [ref=e22]
- columnheader "provider" [ref=e23]
- columnheader "tier" [ref=e24]
- columnheader "status" [ref=e25]
- columnheader "override" [ref=e26]
- rowgroup [ref=e27]:
- row [ref=e28]:
- cell "Loading…" [ref=e29]
- generic [ref=e30]:
- generic [ref=e31]:
- heading "⟁ Recent Decisions" [level=2] [ref=e32]:
- generic [ref=e33]: ⟁
- text: Recent Decisions
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "time" [ref=e38]
- columnheader "kind" [ref=e39]
- columnheader "category" [ref=e40]
- columnheader "tier" [ref=e41]
- columnheader "model" [ref=e42]
- columnheader "cost" [ref=e43]
- columnheader "prof" [ref=e44]
- rowgroup
- generic [ref=e45]:
- heading "▐ Per-Model Usage" [level=2] [ref=e46]:
- generic [ref=e47]: ▐
- text: Per-Model Usage
- generic [ref=e48]: Loading…
- generic [ref=e50]:
- heading "◉ Verdict Mix" [level=2] [ref=e52]:
- generic [ref=e53]: ◉
- text: Verdict Mix
- generic [ref=e56]:
- heading "◆ Category Breakdown" [level=2] [ref=e57]:
- generic [ref=e58]: ◆
- text: Category Breakdown
- generic [ref=e59]: Loading…
- heading "⚠ Warnings" [level=2] [ref=e62]:
- generic [ref=e63]: ⚠
- text: Warnings
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e66]:
- generic [ref=e67]: ◈
- text: History
- generic [ref=e68]:
- button "1h" [ref=e69] [cursor=pointer]
- button "6h" [ref=e70] [cursor=pointer]
- button "24h" [ref=e71] [cursor=pointer]
- button "7d" [ref=e72] [cursor=pointer]
- button "30d" [ref=e73] [cursor=pointer]
- generic [ref=e76]:
- heading "⚙ Controls" [level=2] [ref=e77]:
- generic [ref=e78]: ⚙
- text: Controls
- generic [ref=e79]:
- generic [ref=e80]:
- text: Operational Triggers
- generic [ref=e81]: — fire-and-forget maintenance jobs
- generic [ref=e82]:
- button "↻ Refresh Catalog" [ref=e83] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e84] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e85] [cursor=pointer]
- button "⟳ Restart Service" [ref=e86] [cursor=pointer]
- generic [ref=e89]:
- text: Runtime Knobs
- generic [ref=e90]: — toggle in-memory; no config.yaml write
- generic [ref=e91]:
- generic [ref=e92]:
- text: Persisted Config
- generic [ref=e93]: — allowlisted keys only; persisted to config.yaml
- table [ref=e94]:
- rowgroup [ref=e95]:
- row [ref=e96]:
- cell "Loading config…" [ref=e97]
- button "Save All Config" [ref=e98] [cursor=pointer]

View File

@@ -0,0 +1,725 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 3:17:18 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: 54.6%
- generic [ref=e18]: 54.6%
- generic [ref=e19]: 8149 calls (30d)
- generic [ref=e20]:
- generic [ref=e21]: Plan
- generic [ref=e22]: 6.25 kWh
- generic [ref=e23]: Metered (30d)
- generic [ref=e24]: 3.41404 kWh
- generic [ref=e25]: Calls (30d)
- generic [ref=e26]: "8149"
- generic [ref=e27]: Resets
- generic [ref=e28]: 2026-07-30
- generic [ref=e29]: Note
- generic [ref=e30]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e31]:
- heading "◈ Model Availability" [level=2] [ref=e32]:
- generic [ref=e33]: ◈
- text: Model Availability
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "model" [ref=e38]
- columnheader "provider" [ref=e39]
- columnheader "tier" [ref=e40]
- columnheader "status" [ref=e41]
- columnheader "override" [ref=e42]
- rowgroup [ref=e43]:
- row [ref=e44]:
- cell "deepseek-v4-flash" [ref=e45]
- cell "neuralwatt" [ref=e46]
- cell "2" [ref=e47]
- cell "active" [ref=e48]
- cell "active" [ref=e49]:
- combobox [ref=e50]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e51]:
- cell "deepseek-v4-flash-flex" [ref=e52]
- cell "neuralwatt" [ref=e53]
- cell "2" [ref=e54]
- cell "active" [ref=e55]
- cell "active" [ref=e56]:
- combobox [ref=e57]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e58]:
- cell "gemma-4-31b" [ref=e59]
- cell "neuralwatt" [ref=e60]
- cell "1" [ref=e61]
- cell "active" [ref=e62]
- cell "active" [ref=e63]:
- combobox [ref=e64]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e65]:
- cell "glm-5.2-fast" [ref=e66]
- cell "neuralwatt" [ref=e67]
- cell "2" [ref=e68]
- cell "active" [ref=e69]
- cell "active" [ref=e70]:
- combobox [ref=e71]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e72]:
- cell "glm-5.2-flex" [ref=e73]
- cell "neuralwatt" [ref=e74]
- cell "3" [ref=e75]
- cell "active" [ref=e76]
- cell "active" [ref=e77]:
- combobox [ref=e78]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e79]:
- cell "glm-5.3" [ref=e80]
- cell "neuralwatt" [ref=e81]
- cell "3" [ref=e82]
- cell "active" [ref=e83]
- cell "active" [ref=e84]:
- combobox [ref=e85]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e86]:
- cell "kimi-k2.7-code" [ref=e87]
- cell "neuralwatt" [ref=e88]
- cell "3" [ref=e89]
- cell "active" [ref=e90]
- cell "active" [ref=e91]:
- combobox [ref=e92]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e93]:
- cell "kimi-k2.7-code-fast" [ref=e94]
- cell "neuralwatt" [ref=e95]
- cell "2" [ref=e96]
- cell "active" [ref=e97]
- cell "active" [ref=e98]:
- combobox [ref=e99]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e100]:
- cell "kimi-k2.7-code-flex" [ref=e101]
- cell "neuralwatt" [ref=e102]
- cell "3" [ref=e103]
- cell "active" [ref=e104]
- cell "active" [ref=e105]:
- combobox [ref=e106]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e107]:
- cell "kimi-k3" [ref=e108]
- cell "neuralwatt" [ref=e109]
- cell "3" [ref=e110]
- cell "active" [ref=e111]
- cell "active" [ref=e112]:
- combobox [ref=e113]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e114]:
- cell "kimi-k3-fast" [ref=e115]
- cell "neuralwatt" [ref=e116]
- cell "2" [ref=e117]
- cell "active" [ref=e118]
- cell "active" [ref=e119]:
- combobox [ref=e120]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e121]:
- cell "kimi-k3-flex" [ref=e122]
- cell "neuralwatt" [ref=e123]
- cell "3" [ref=e124]
- cell "active" [ref=e125]
- cell "active" [ref=e126]:
- combobox [ref=e127]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e128]:
- cell "qwen3.6-35b" [ref=e129]
- cell "neuralwatt" [ref=e130]
- cell "3" [ref=e131]
- cell "active" [ref=e132]
- cell "active" [ref=e133]:
- combobox [ref=e134]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e135]:
- cell "qwen3.6-35b-fast" [ref=e136]
- cell "neuralwatt" [ref=e137]
- cell "2" [ref=e138]
- cell "active" [ref=e139]
- cell "active" [ref=e140]:
- combobox [ref=e141]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- generic [ref=e142]:
- generic [ref=e143]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e144]:
- generic [ref=e145]: ⟁
- text: Recent Decisions
- generic [ref=e146]: (50)
- table [ref=e148]:
- rowgroup [ref=e149]:
- row [ref=e150]:
- columnheader "time" [ref=e151]
- columnheader "kind" [ref=e152]
- columnheader "category" [ref=e153]
- columnheader "tier" [ref=e154]
- columnheader "model" [ref=e155]
- columnheader "cost" [ref=e156]
- columnheader "prof" [ref=e157]
- rowgroup [ref=e158]:
- row [ref=e159]:
- cell "03:14:45 AM" [ref=e160]
- cell "chat" [ref=e161]
- cell "reasoning_math" [ref=e163]
- cell "2" [ref=e164]
- cell "qwen3.6-35b / neuralwatt" [ref=e165]
- cell "$0.0036" [ref=e166]
- cell "1.00" [ref=e167]
- row [ref=e168]:
- cell "03:14:48 AM" [ref=e169]
- cell "chat" [ref=e170]
- cell "tool_use_agentic" [ref=e172]
- cell "2" [ref=e173]
- cell "qwen3.6-35b / neuralwatt" [ref=e174]
- cell "$0.0038" [ref=e175]
- cell "1.00" [ref=e176]
- row [ref=e177]:
- cell "03:14:50 AM" [ref=e178]
- cell "chat" [ref=e179]
- cell "reasoning_math" [ref=e181]
- cell "2" [ref=e182]
- cell "qwen3.6-35b / neuralwatt" [ref=e183]
- cell "$0.0036" [ref=e184]
- cell "1.00" [ref=e185]
- row [ref=e186]:
- cell "03:14:52 AM" [ref=e187]
- cell "chat" [ref=e188]
- cell "coding_general" [ref=e190]
- cell "2" [ref=e191]
- cell "deepseek-v4-flash / neuralwatt" [ref=e192]
- cell "$0.0031" [ref=e193]
- cell "1.00" [ref=e194]
- row [ref=e195]:
- cell "03:14:55 AM" [ref=e196]
- cell "chat" [ref=e197]
- cell "debugging" [ref=e199]
- cell "2" [ref=e200]
- cell "deepseek-v4-flash / neuralwatt" [ref=e201]
- cell "$0.0026" [ref=e202]
- cell "1.00" [ref=e203]
- row [ref=e204]:
- cell "03:14:57 AM" [ref=e205]
- cell "chat" [ref=e206]
- cell "coding_general" [ref=e208]
- cell "2" [ref=e209]
- cell "deepseek-v4-flash / neuralwatt" [ref=e210]
- cell "$0.0032" [ref=e211]
- cell "1.00" [ref=e212]
- row [ref=e213]:
- cell "03:14:58 AM" [ref=e214]
- cell "chat" [ref=e215]
- cell "reasoning_math" [ref=e217]
- cell "2" [ref=e218]
- cell "qwen3.6-35b / neuralwatt" [ref=e219]
- cell "$0.0036" [ref=e220]
- cell "1.00" [ref=e221]
- row [ref=e222]:
- cell "03:15:01 AM" [ref=e223]
- cell "chat" [ref=e224]
- cell "debugging" [ref=e226]
- cell "2" [ref=e227]
- cell "deepseek-v4-flash / neuralwatt" [ref=e228]
- cell "$0.0027" [ref=e229]
- cell "1.00" [ref=e230]
- row [ref=e231]:
- cell "03:15:02 AM" [ref=e232]
- cell "chat" [ref=e233]
- cell "coding_general" [ref=e235]
- cell "2" [ref=e236]
- cell "deepseek-v4-flash / neuralwatt" [ref=e237]
- cell "$0.0032" [ref=e238]
- cell "1.00" [ref=e239]
- row [ref=e240]:
- cell "03:15:05 AM" [ref=e241]
- cell "chat" [ref=e242]
- cell "debugging" [ref=e244]
- cell "2" [ref=e245]
- cell "deepseek-v4-flash / neuralwatt" [ref=e246]
- cell "$0.0027" [ref=e247]
- cell "1.00" [ref=e248]
- row [ref=e249]:
- cell "03:15:10 AM" [ref=e250]
- cell "chat" [ref=e251]
- cell "coding_refactor" [ref=e253]
- cell "2" [ref=e254]
- cell "deepseek-v4-flash / neuralwatt" [ref=e255]
- cell "$0.0032" [ref=e256]
- cell "1.00" [ref=e257]
- row [ref=e258]:
- cell "03:15:16 AM" [ref=e259]
- cell "chat" [ref=e260]
- cell "tool_use_agentic" [ref=e262]
- cell "2" [ref=e263]
- cell "qwen3.6-35b / neuralwatt" [ref=e264]
- cell "$0.0040" [ref=e265]
- cell "1.00" [ref=e266]
- row [ref=e267]:
- cell "03:15:18 AM" [ref=e268]
- cell "chat" [ref=e269]
- cell "coding_general" [ref=e271]
- cell "2" [ref=e272]
- cell "deepseek-v4-flash / neuralwatt" [ref=e273]
- cell "$0.0032" [ref=e274]
- cell "1.00" [ref=e275]
- row [ref=e276]:
- cell "03:15:21 AM" [ref=e277]
- cell "chat" [ref=e278]
- cell "general_chat" [ref=e280]
- cell "2" [ref=e281]
- cell "deepseek-v4-flash / neuralwatt" [ref=e282]
- cell "$0.0028" [ref=e283]
- cell "1.00" [ref=e284]
- row [ref=e285]:
- cell "03:15:22 AM" [ref=e286]
- cell "chat" [ref=e287]
- cell "tool_use_agentic" [ref=e289]
- cell "3" [ref=e290]
- cell "qwen3.6-35b / neuralwatt" [ref=e291]
- cell "$0.0040" [ref=e292]
- cell "1.00" [ref=e293]
- row [ref=e294]:
- cell "03:15:25 AM" [ref=e295]
- cell "chat" [ref=e296]
- cell "debugging" [ref=e298]
- cell "2" [ref=e299]
- cell "deepseek-v4-flash / neuralwatt" [ref=e300]
- cell "$0.0029" [ref=e301]
- cell "1.00" [ref=e302]
- row [ref=e303]:
- cell "03:15:27 AM" [ref=e304]
- cell "chat" [ref=e305]
- cell "tool_use_agentic" [ref=e307]
- cell "3" [ref=e308]
- cell "qwen3.6-35b / neuralwatt" [ref=e309]
- cell "$0.0040" [ref=e310]
- cell "1.00" [ref=e311]
- row [ref=e312]:
- cell "03:15:28 AM" [ref=e313]
- cell "chat" [ref=e314]
- cell "reasoning_math" [ref=e316]
- cell "2" [ref=e317]
- cell "qwen3.6-35b / neuralwatt" [ref=e318]
- cell "$0.0044" [ref=e319]
- cell "1.00" [ref=e320]
- row [ref=e321]:
- cell "03:15:35 AM" [ref=e322]
- cell "chat" [ref=e323]
- cell "tool_use_agentic" [ref=e325]
- cell "3" [ref=e326]
- cell "qwen3.6-35b / neuralwatt" [ref=e327]
- cell "$0.0040" [ref=e328]
- cell "1.00" [ref=e329]
- row [ref=e330]:
- cell "03:15:38 AM" [ref=e331]
- cell "chat" [ref=e332]
- cell "tool_use_agentic" [ref=e334]
- cell "2" [ref=e335]
- cell "qwen3.6-35b / neuralwatt" [ref=e336]
- cell "$0.0046" [ref=e337]
- cell "1.00" [ref=e338]
- row [ref=e339]:
- cell "03:15:40 AM" [ref=e340]
- cell "chat" [ref=e341]
- cell "tool_use_agentic" [ref=e343]
- cell "3" [ref=e344]
- cell "qwen3.6-35b / neuralwatt" [ref=e345]
- cell "$0.0040" [ref=e346]
- cell "1.00" [ref=e347]
- row [ref=e348]:
- cell "03:15:49 AM" [ref=e349]
- cell "chat" [ref=e350]
- cell "tool_use_agentic" [ref=e352]
- cell "3" [ref=e353]
- cell "qwen3.6-35b / neuralwatt" [ref=e354]
- cell "$0.0044" [ref=e355]
- cell "1.00" [ref=e356]
- row [ref=e357]:
- cell "03:15:50 AM" [ref=e358]
- cell "chat" [ref=e359]
- cell "debugging" [ref=e361]
- cell "2" [ref=e362]
- cell "deepseek-v4-flash / neuralwatt" [ref=e363]
- cell "$0.0031" [ref=e364]
- cell "1.00" [ref=e365]
- row [ref=e366]:
- cell "03:15:56 AM" [ref=e367]
- cell "chat" [ref=e368]
- cell "debugging" [ref=e370]
- cell "2" [ref=e371]
- cell "deepseek-v4-flash / neuralwatt" [ref=e372]
- cell "$0.0031" [ref=e373]
- cell "1.00" [ref=e374]
- row [ref=e375]:
- cell "03:15:58 AM" [ref=e376]
- cell "chat" [ref=e377]
- cell "tool_use_agentic" [ref=e379]
- cell "3" [ref=e380]
- cell "qwen3.6-35b / neuralwatt" [ref=e381]
- cell "$0.0044" [ref=e382]
- cell "1.00" [ref=e383]
- row [ref=e384]:
- cell "03:16:00 AM" [ref=e385]
- cell "chat" [ref=e386]
- cell "coding_general" [ref=e388]
- cell "2" [ref=e389]
- cell "deepseek-v4-flash / neuralwatt" [ref=e390]
- cell "$0.0033" [ref=e391]
- cell "1.00" [ref=e392]
- row [ref=e393]:
- cell "03:16:02 AM" [ref=e394]
- cell "chat" [ref=e395]
- cell "reasoning_math" [ref=e397]
- cell "2" [ref=e398]
- cell "qwen3.6-35b / neuralwatt" [ref=e399]
- cell "$0.0047" [ref=e400]
- cell "1.00" [ref=e401]
- row [ref=e402]:
- cell "03:16:03 AM" [ref=e403]
- cell "chat" [ref=e404]
- cell "tool_use_agentic" [ref=e406]
- cell "3" [ref=e407]
- cell "qwen3.6-35b / neuralwatt" [ref=e408]
- cell "$0.0045" [ref=e409]
- cell "1.00" [ref=e410]
- row [ref=e411]:
- cell "03:16:05 AM" [ref=e412]
- cell "chat" [ref=e413]
- cell "coding_general" [ref=e415]
- cell "2" [ref=e416]
- cell "deepseek-v4-flash / neuralwatt" [ref=e417]
- cell "$0.0033" [ref=e418]
- cell "1.00" [ref=e419]
- row [ref=e420]:
- cell "03:16:08 AM" [ref=e421]
- cell "chat" [ref=e422]
- cell "debugging" [ref=e424]
- cell "2" [ref=e425]
- cell "deepseek-v4-flash / neuralwatt" [ref=e426]
- cell "$0.0032" [ref=e427]
- cell "1.00" [ref=e428]
- row [ref=e429]:
- cell "03:16:09 AM" [ref=e430]
- cell "chat" [ref=e431]
- cell "tool_use_agentic" [ref=e433]
- cell "2" [ref=e434]
- cell "qwen3.6-35b / neuralwatt" [ref=e435]
- cell "$0.0010" [ref=e436]
- cell "1.00" [ref=e437]
- row [ref=e438]:
- cell "03:16:11 AM" [ref=e439]
- cell "chat" [ref=e440]
- cell "coding_general" [ref=e442]
- cell "2" [ref=e443]
- cell "deepseek-v4-flash / neuralwatt" [ref=e444]
- cell "$0.0034" [ref=e445]
- cell "1.00" [ref=e446]
- row [ref=e447]:
- cell "03:16:14 AM" [ref=e448]
- cell "chat" [ref=e449]
- cell "tool_use_agentic" [ref=e451]
- cell "2" [ref=e452]
- cell "qwen3.6-35b / neuralwatt" [ref=e453]
- cell "$0.0010" [ref=e454]
- cell "1.00" [ref=e455]
- row [ref=e456]:
- cell "03:16:15 AM" [ref=e457]
- cell "chat" [ref=e458]
- cell "debugging" [ref=e460]
- cell "2" [ref=e461]
- cell "deepseek-v4-flash / neuralwatt" [ref=e462]
- cell "$0.0033" [ref=e463]
- cell "1.00" [ref=e464]
- row [ref=e465]:
- cell "03:16:19 AM" [ref=e466]
- cell "chat" [ref=e467]
- cell "tool_use_agentic" [ref=e469]
- cell "2" [ref=e470]
- cell "qwen3.6-35b / neuralwatt" [ref=e471]
- cell "$0.0017" [ref=e472]
- cell "1.00" [ref=e473]
- row [ref=e474]:
- cell "03:16:21 AM" [ref=e475]
- cell "chat" [ref=e476]
- cell "coding_refactor" [ref=e478]
- cell "2" [ref=e479]
- cell "deepseek-v4-flash / neuralwatt" [ref=e480]
- cell "$0.0035" [ref=e481]
- cell "1.00" [ref=e482]
- row [ref=e483]:
- cell "03:16:24 AM" [ref=e484]
- cell "chat" [ref=e485]
- cell "reasoning_math" [ref=e487]
- cell "2" [ref=e488]
- cell "qwen3.6-35b / neuralwatt" [ref=e489]
- cell "$0.0049" [ref=e490]
- cell "1.00" [ref=e491]
- row [ref=e492]:
- cell "03:16:25 AM" [ref=e493]
- cell "chat" [ref=e494]
- cell "tool_use_agentic" [ref=e496]
- cell "2" [ref=e497]
- cell "qwen3.6-35b / neuralwatt" [ref=e498]
- cell "$0.0023" [ref=e499]
- cell "1.00" [ref=e500]
- row [ref=e501]:
- cell "03:16:27 AM" [ref=e502]
- cell "chat" [ref=e503]
- cell "coding_general" [ref=e505]
- cell "2" [ref=e506]
- cell "deepseek-v4-flash / neuralwatt" [ref=e507]
- cell "$0.0035" [ref=e508]
- cell "1.00" [ref=e509]
- row [ref=e510]:
- cell "03:16:29 AM" [ref=e511]
- cell "chat" [ref=e512]
- cell "tool_use_agentic" [ref=e514]
- cell "2" [ref=e515]
- cell "qwen3.6-35b / neuralwatt" [ref=e516]
- cell "$0.0023" [ref=e517]
- cell "1.00" [ref=e518]
- row [ref=e519]:
- cell "03:16:31 AM" [ref=e520]
- cell "chat" [ref=e521]
- cell "reasoning_math" [ref=e523]
- cell "2" [ref=e524]
- cell "qwen3.6-35b / neuralwatt" [ref=e525]
- cell "$0.0049" [ref=e526]
- cell "1.00" [ref=e527]
- row [ref=e528]:
- cell "03:16:33 AM" [ref=e529]
- cell "chat" [ref=e530]
- cell "tool_use_agentic" [ref=e532]
- cell "2" [ref=e533]
- cell "qwen3.6-35b / neuralwatt" [ref=e534]
- cell "$0.0025" [ref=e535]
- cell "1.00" [ref=e536]
- row [ref=e537]:
- cell "03:16:38 AM" [ref=e538]
- cell "chat" [ref=e539]
- cell "tool_use_agentic" [ref=e541]
- cell "2" [ref=e542]
- cell "qwen3.6-35b / neuralwatt" [ref=e543]
- cell "$0.0025" [ref=e544]
- cell "1.00" [ref=e545]
- row [ref=e546]:
- cell "03:16:39 AM" [ref=e547]
- cell "chat" [ref=e548]
- cell "debugging" [ref=e550]
- cell "2" [ref=e551]
- cell "deepseek-v4-flash / neuralwatt" [ref=e552]
- cell "$0.0033" [ref=e553]
- cell "1.00" [ref=e554]
- row [ref=e555]:
- cell "03:16:41 AM" [ref=e556]
- cell "chat" [ref=e557]
- cell "tool_use_agentic" [ref=e559]
- cell "2" [ref=e560]
- cell "qwen3.6-35b / neuralwatt" [ref=e561]
- cell "$0.0025" [ref=e562]
- cell "1.00" [ref=e563]
- row [ref=e564]:
- cell "03:16:46 AM" [ref=e565]
- cell "chat" [ref=e566]
- cell "general_chat" [ref=e568]
- cell "2" [ref=e569]
- cell "deepseek-v4-flash / neuralwatt" [ref=e570]
- cell "$0.0034" [ref=e571]
- cell "1.00" [ref=e572]
- row [ref=e573]:
- cell "03:16:47 AM" [ref=e574]
- cell "chat" [ref=e575]
- cell "tool_use_agentic" [ref=e577]
- cell "2" [ref=e578]
- cell "qwen3.6-35b / neuralwatt" [ref=e579]
- cell "$0.0032" [ref=e580]
- cell "1.00" [ref=e581]
- row [ref=e582]:
- cell "03:16:52 AM" [ref=e583]
- cell "chat" [ref=e584]
- cell "tool_use_agentic" [ref=e586]
- cell "2" [ref=e587]
- cell "qwen3.6-35b / neuralwatt" [ref=e588]
- cell "$0.0032" [ref=e589]
- cell "1.00" [ref=e590]
- row [ref=e591]:
- cell "03:16:56 AM" [ref=e592]
- cell "chat" [ref=e593]
- cell "tool_use_agentic" [ref=e595]
- cell "2" [ref=e596]
- cell "qwen3.6-35b / neuralwatt" [ref=e597]
- cell "$0.0038" [ref=e598]
- cell "1.00" [ref=e599]
- row [ref=e600]:
- cell "03:17:11 AM" [ref=e601]
- cell "chat" [ref=e602]
- cell "tool_use_agentic" [ref=e604]
- cell "3" [ref=e605]
- cell "qwen3.6-35b / neuralwatt" [ref=e606]
- cell "$0.0046" [ref=e607]
- cell "1.00" [ref=e608]
- generic [ref=e609]:
- heading "▐ Per-Model Usage" [level=2] [ref=e610]:
- generic [ref=e611]: ▐
- text: Per-Model Usage
- generic [ref=e612]:
- generic [ref=e613]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e614]
- generic [ref=e615]: "4296"
- generic [ref=e617]: $8.3528 · 1.10696kWh
- generic [ref=e618]:
- generic "qwen3.6-35b / neuralwatt" [ref=e619]
- generic [ref=e620]: "1423"
- generic [ref=e622]: $1.8218 · 0.22777kWh
- generic [ref=e623]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e624]
- generic [ref=e625]: "892"
- generic [ref=e627]: $5.2875 · 0.66120kWh
- generic [ref=e628]:
- generic "gemma-4-31b / neuralwatt" [ref=e629]
- generic [ref=e630]: "380"
- generic [ref=e632]: $1.0993 · 0.16170kWh
- generic [ref=e633]:
- generic "glm-5.2-fast / neuralwatt" [ref=e634]
- generic [ref=e635]: "355"
- generic [ref=e637]: $4.9416 · 0.61835kWh
- generic [ref=e638]:
- generic "kimi-k3 / neuralwatt" [ref=e639]
- generic [ref=e640]: "148"
- generic [ref=e642]: $3.1208 · 0.39010kWh
- generic [ref=e643]:
- generic "glm-5.2-flex / neuralwatt" [ref=e644]
- generic [ref=e645]: "127"
- generic [ref=e647]: $0.6401 · 0.12080kWh
- generic [ref=e648]:
- generic "kimi-k3-fast / neuralwatt" [ref=e649]
- generic [ref=e650]: "96"
- generic [ref=e652]: $0.5209 · 0.06511kWh
- generic [ref=e653]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e654]
- generic [ref=e655]: "85"
- generic [ref=e657]: $0.0129 · 0.00324kWh
- generic [ref=e658]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e659]
- generic [ref=e660]: "85"
- generic [ref=e662]: $0.0779 · 0.00974kWh
- generic [ref=e663]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e664]
- generic [ref=e665]: "85"
- generic [ref=e667]: $0.0903 · 0.01129kWh
- generic [ref=e668]:
- generic "kimi-k3-flex / neuralwatt" [ref=e669]
- generic [ref=e670]: "85"
- generic [ref=e672]: $0.2678 · 0.03348kWh
- generic [ref=e673]:
- generic [ref=e674]:
- heading "◉ Verdict Mix" [level=2] [ref=e675]:
- generic [ref=e676]: ◉
- text: Verdict Mix
- generic [ref=e679]:
- generic [ref=e680]: "failed: 114"
- generic [ref=e682]: "malformed: 51"
- generic [ref=e684]: "ok: 179"
- generic [ref=e686]: "succeeded: 536"
- generic [ref=e688]: "truncated: 9"
- generic [ref=e690]: "unverifiable: 6824"
- generic [ref=e692]:
- heading "◆ Category Breakdown" [level=2] [ref=e693]:
- generic [ref=e694]: ◆
- text: Category Breakdown
- generic [ref=e695]:
- generic [ref=e696]:
- generic [ref=e698]: tool_use_agentic
- generic [ref=e699]: "22"
- generic [ref=e700]:
- generic [ref=e702]: debugging
- generic [ref=e703]: "9"
- generic [ref=e704]:
- generic [ref=e706]: coding_general
- generic [ref=e707]: "8"
- generic [ref=e708]:
- generic [ref=e710]: reasoning_math
- generic [ref=e711]: "7"
- generic [ref=e712]:
- generic [ref=e714]: general_chat
- generic [ref=e715]: "2"
- generic [ref=e716]:
- generic [ref=e718]: coding_refactor
- generic [ref=e719]: "2"
- generic [ref=e720]:
- heading "⚠ Warnings" [level=2] [ref=e721]:
- generic [ref=e722]: ⚠
- text: Warnings
- generic [ref=e723]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e727]:
- generic [ref=e728]: ◈
- text: History
- generic [ref=e729]:
- button "1h" [ref=e730] [cursor=pointer]
- button "6h" [ref=e731] [cursor=pointer]
- button "24h" [ref=e732] [cursor=pointer]
- button "7d" [ref=e733] [cursor=pointer]
- button "30d" [ref=e734] [cursor=pointer]
- generic [ref=e737]:
- heading "⚙ Controls" [level=2] [ref=e738]:
- generic [ref=e739]: ⚙
- text: Controls
- generic [ref=e740]:
- generic [ref=e741]:
- text: Operational Triggers
- generic [ref=e742]: — fire-and-forget maintenance jobs
- generic [ref=e743]:
- button "↻ Refresh Catalog" [ref=e744] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e745] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e746] [cursor=pointer]
- button "⟳ Restart Service" [ref=e747] [cursor=pointer]
- generic [ref=e750]:
- text: Runtime Knobs
- generic [ref=e751]: — toggle in-memory; no config.yaml write
- generic [ref=e752]:
- generic [ref=e753]:
- text: Persisted Config
- generic [ref=e754]: — allowlisted keys only; persisted to config.yaml
- table [ref=e755]:
- rowgroup [ref=e756]:
- row [ref=e757]:
- cell "Loading config…" [ref=e758]
- button "Save All Config" [ref=e759] [cursor=pointer]

View File

@@ -0,0 +1,95 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: —
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e13]: Loading…
- generic [ref=e15]:
- heading "◈ Model Availability" [level=2] [ref=e16]:
- generic [ref=e17]: ◈
- text: Model Availability
- table [ref=e19]:
- rowgroup [ref=e20]:
- row [ref=e21]:
- columnheader "model" [ref=e22]
- columnheader "provider" [ref=e23]
- columnheader "tier" [ref=e24]
- columnheader "status" [ref=e25]
- columnheader "override" [ref=e26]
- rowgroup [ref=e27]:
- row [ref=e28]:
- cell "Loading…" [ref=e29]
- generic [ref=e30]:
- generic [ref=e31]:
- heading "⟁ Recent Decisions" [level=2] [ref=e32]:
- generic [ref=e33]: ⟁
- text: Recent Decisions
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "time" [ref=e38]
- columnheader "kind" [ref=e39]
- columnheader "category" [ref=e40]
- columnheader "tier" [ref=e41]
- columnheader "model" [ref=e42]
- columnheader "cost" [ref=e43]
- columnheader "prof" [ref=e44]
- rowgroup
- generic [ref=e45]:
- heading "▐ Per-Model Usage" [level=2] [ref=e46]:
- generic [ref=e47]: ▐
- text: Per-Model Usage
- generic [ref=e48]: Loading…
- generic [ref=e50]:
- heading "◉ Verdict Mix" [level=2] [ref=e52]:
- generic [ref=e53]: ◉
- text: Verdict Mix
- generic [ref=e56]:
- heading "◆ Category Breakdown" [level=2] [ref=e57]:
- generic [ref=e58]: ◆
- text: Category Breakdown
- generic [ref=e59]: Loading…
- heading "⚠ Warnings" [level=2] [ref=e62]:
- generic [ref=e63]: ⚠
- text: Warnings
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e66]:
- generic [ref=e67]: ◈
- text: History
- generic [ref=e68]:
- button "1h" [ref=e69] [cursor=pointer]
- button "6h" [ref=e70] [cursor=pointer]
- button "24h" [ref=e71] [cursor=pointer]
- button "7d" [ref=e72] [cursor=pointer]
- button "30d" [ref=e73] [cursor=pointer]
- generic [ref=e76]:
- heading "⚙ Controls" [level=2] [ref=e77]:
- generic [ref=e78]: ⚙
- text: Controls
- generic [ref=e79]:
- generic [ref=e80]:
- text: Operational Triggers
- generic [ref=e81]: — fire-and-forget maintenance jobs
- generic [ref=e82]:
- button "↻ Refresh Catalog" [ref=e83] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e84] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e85] [cursor=pointer]
- button "⟳ Restart Service" [ref=e86] [cursor=pointer]
- generic [ref=e89]:
- text: Runtime Knobs
- generic [ref=e90]: — toggle in-memory; no config.yaml write
- generic [ref=e91]:
- generic [ref=e92]:
- text: Persisted Config
- generic [ref=e93]: — allowlisted keys only; persisted to config.yaml
- table [ref=e94]:
- rowgroup [ref=e95]:
- row [ref=e96]:
- cell "Loading config…" [ref=e97]
- button "Save All Config" [ref=e98] [cursor=pointer]

View File

@@ -0,0 +1,812 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 10:42:48 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e99]:
- generic [ref=e100]:
- generic [ref=e101]: 54.9%
- generic [ref=e103]: 54.9%
- generic [ref=e104]: 8254 calls (30d)
- generic [ref=e105]:
- generic [ref=e106]: Plan
- generic [ref=e107]: 6.25 kWh
- generic [ref=e108]: Metered (30d)
- generic [ref=e109]: 3.43118 kWh
- generic [ref=e110]: Calls (30d)
- generic [ref=e111]: "8254"
- generic [ref=e112]: Resets
- generic [ref=e113]: 2026-07-30
- generic [ref=e114]: Note
- generic [ref=e115]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e15]:
- heading "◈ Model Availability" [level=2] [ref=e16]:
- generic [ref=e17]: ◈
- text: Model Availability
- table [ref=e19]:
- rowgroup [ref=e20]:
- row [ref=e21]:
- columnheader "model" [ref=e22]
- columnheader "provider" [ref=e23]
- columnheader "tier" [ref=e24]
- columnheader "status" [ref=e25]
- columnheader "override" [ref=e26]
- rowgroup [ref=e27]:
- row [ref=e116]:
- cell "deepseek-v4-flash" [ref=e117]
- cell "neuralwatt" [ref=e118]
- cell "2" [ref=e119]
- cell "active" [ref=e120]
- cell "active" [ref=e121]:
- combobox [ref=e122]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e123]:
- cell "deepseek-v4-flash-flex" [ref=e124]
- cell "neuralwatt" [ref=e125]
- cell "2" [ref=e126]
- cell "active" [ref=e127]
- cell "active" [ref=e128]:
- combobox [ref=e129]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e130]:
- cell "gemma-4-31b" [ref=e131]
- cell "neuralwatt" [ref=e132]
- cell "1" [ref=e133]
- cell "active" [ref=e134]
- cell "active" [ref=e135]:
- combobox [ref=e136]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e137]:
- cell "glm-5.2-fast" [ref=e138]
- cell "neuralwatt" [ref=e139]
- cell "2" [ref=e140]
- cell "active" [ref=e141]
- cell "active" [ref=e142]:
- combobox [ref=e143]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e144]:
- cell "glm-5.2-flex" [ref=e145]
- cell "neuralwatt" [ref=e146]
- cell "3" [ref=e147]
- cell "active" [ref=e148]
- cell "active" [ref=e149]:
- combobox [ref=e150]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e151]:
- cell "glm-5.3" [ref=e152]
- cell "neuralwatt" [ref=e153]
- cell "3" [ref=e154]
- cell "active" [ref=e155]
- cell "active" [ref=e156]:
- combobox [ref=e157]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e158]:
- cell "kimi-k2.7-code" [ref=e159]
- cell "neuralwatt" [ref=e160]
- cell "3" [ref=e161]
- cell "active" [ref=e162]
- cell "active" [ref=e163]:
- combobox [ref=e164]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e165]:
- cell "kimi-k2.7-code-fast" [ref=e166]
- cell "neuralwatt" [ref=e167]
- cell "2" [ref=e168]
- cell "active" [ref=e169]
- cell "active" [ref=e170]:
- combobox [ref=e171]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e172]:
- cell "kimi-k2.7-code-flex" [ref=e173]
- cell "neuralwatt" [ref=e174]
- cell "3" [ref=e175]
- cell "active" [ref=e176]
- cell "active" [ref=e177]:
- combobox [ref=e178]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e179]:
- cell "kimi-k3" [ref=e180]
- cell "neuralwatt" [ref=e181]
- cell "3" [ref=e182]
- cell "active" [ref=e183]
- cell "active" [ref=e184]:
- combobox [ref=e185]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e186]:
- cell "kimi-k3-fast" [ref=e187]
- cell "neuralwatt" [ref=e188]
- cell "2" [ref=e189]
- cell "active" [ref=e190]
- cell "active" [ref=e191]:
- combobox [ref=e192]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e193]:
- cell "kimi-k3-flex" [ref=e194]
- cell "neuralwatt" [ref=e195]
- cell "3" [ref=e196]
- cell "active" [ref=e197]
- cell "active" [ref=e198]:
- combobox [ref=e199]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e200]:
- cell "qwen3.6-35b" [ref=e201]
- cell "neuralwatt" [ref=e202]
- cell "3" [ref=e203]
- cell "active" [ref=e204]
- cell "active" [ref=e205]:
- combobox [ref=e206]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e207]:
- cell "qwen3.6-35b-fast" [ref=e208]
- cell "neuralwatt" [ref=e209]
- cell "2" [ref=e210]
- cell "active" [ref=e211]
- cell "active" [ref=e212]:
- combobox [ref=e213]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- generic [ref=e30]:
- generic [ref=e31]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e214]:
- generic [ref=e33]: ⟁
- text: Recent Decisions
- generic [ref=e215]: (50)
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "time" [ref=e38]
- columnheader "kind" [ref=e39]
- columnheader "category" [ref=e40]
- columnheader "tier" [ref=e41]
- columnheader "model" [ref=e42]
- columnheader "cost" [ref=e43]
- columnheader "prof" [ref=e44]
- rowgroup [ref=e216]:
- row [ref=e217]:
- cell "03:18:26 AM" [ref=e218]
- cell "chat" [ref=e219]
- cell "tool_use_agentic" [ref=e221]
- cell "3" [ref=e222]
- cell "qwen3.6-35b / neuralwatt" [ref=e223]
- cell "$0.0053" [ref=e224]
- cell "1.00" [ref=e225]
- row [ref=e226]:
- cell "03:18:32 AM" [ref=e227]
- cell "chat" [ref=e228]
- cell "tool_use_agentic" [ref=e230]
- cell "3" [ref=e231]
- cell "qwen3.6-35b / neuralwatt" [ref=e232]
- cell "$0.0053" [ref=e233]
- cell "1.00" [ref=e234]
- row [ref=e235]:
- cell "03:18:41 AM" [ref=e236]
- cell "chat" [ref=e237]
- cell "tool_use_agentic" [ref=e239]
- cell "3" [ref=e240]
- cell "qwen3.6-35b / neuralwatt" [ref=e241]
- cell "$0.0053" [ref=e242]
- cell "1.00" [ref=e243]
- row [ref=e244]:
- cell "03:18:47 AM" [ref=e245]
- cell "chat" [ref=e246]
- cell "tool_use_agentic" [ref=e248]
- cell "3" [ref=e249]
- cell "kimi-k2.7-code / neuralwatt" [ref=e250]
- cell "$0.0176" [ref=e251]
- cell "1.00" [ref=e252]
- row [ref=e253]:
- cell "03:19:02 AM" [ref=e254]
- cell "chat" [ref=e255]
- cell "tool_use_agentic" [ref=e257]
- cell "3" [ref=e258]
- cell "kimi-k2.7-code / neuralwatt" [ref=e259]
- cell "$0.0177" [ref=e260]
- cell "1.00" [ref=e261]
- row [ref=e262]:
- cell "03:19:08 AM" [ref=e263]
- cell "chat" [ref=e264]
- cell "tool_use_agentic" [ref=e266]
- cell "3" [ref=e267]
- cell "kimi-k2.7-code / neuralwatt" [ref=e268]
- cell "$0.0177" [ref=e269]
- cell "1.00" [ref=e270]
- row [ref=e271]:
- cell "03:19:13 AM" [ref=e272]
- cell "chat" [ref=e273]
- cell "tool_use_agentic" [ref=e275]
- cell "3" [ref=e276]
- cell "kimi-k2.7-code / neuralwatt" [ref=e277]
- cell "$0.0178" [ref=e278]
- cell "1.00" [ref=e279]
- row [ref=e280]:
- cell "03:19:17 AM" [ref=e281]
- cell "chat" [ref=e282]
- cell "tool_use_agentic" [ref=e284]
- cell "3" [ref=e285]
- cell "kimi-k2.7-code / neuralwatt" [ref=e286]
- cell "$0.0179" [ref=e287]
- cell "1.00" [ref=e288]
- row [ref=e289]:
- cell "03:19:22 AM" [ref=e290]
- cell "chat" [ref=e291]
- cell "general_chat" [ref=e293]
- cell "2" [ref=e294]
- cell "deepseek-v4-flash / neuralwatt" [ref=e295]
- cell "$0.0037" [ref=e296]
- cell "1.00" [ref=e297]
- row [ref=e298]:
- cell "03:19:36 AM" [ref=e299]
- cell "chat" [ref=e300]
- cell "general_chat" [ref=e302]
- cell "3" [ref=e303]
- cell "kimi-k2.7-code / neuralwatt" [ref=e304]
- cell "$0.0179" [ref=e305]
- cell "0.80" [ref=e306]
- row [ref=e307]:
- cell "03:20:19 AM" [ref=e308]
- cell "chat" [ref=e309]
- cell "general_chat" [ref=e311]
- cell "2" [ref=e312]
- cell "deepseek-v4-flash / neuralwatt" [ref=e313]
- cell "$0.0037" [ref=e314]
- cell "1.00" [ref=e315]
- row [ref=e316]:
- cell "03:20:25 AM" [ref=e317]
- cell "chat" [ref=e318]
- cell "general_chat" [ref=e320]
- cell "2" [ref=e321]
- cell "deepseek-v4-flash / neuralwatt" [ref=e322]
- cell "$0.0038" [ref=e323]
- cell "1.00" [ref=e324]
- row [ref=e325]:
- cell "03:20:31 AM" [ref=e326]
- cell "chat" [ref=e327]
- cell "tool_use_agentic" [ref=e329]
- cell "3" [ref=e330]
- cell "kimi-k2.7-code / neuralwatt" [ref=e331]
- cell "$0.0184" [ref=e332]
- cell "1.00" [ref=e333]
- row [ref=e334]:
- cell "03:20:38 AM" [ref=e335]
- cell "chat" [ref=e336]
- cell "general_chat" [ref=e338]
- cell "2" [ref=e339]
- cell "deepseek-v4-flash / neuralwatt" [ref=e340]
- cell "$0.0038" [ref=e341]
- cell "1.00" [ref=e342]
- row [ref=e343]:
- cell "03:20:47 AM" [ref=e344]
- cell "chat" [ref=e345]
- cell "general_chat" [ref=e347]
- cell "2" [ref=e348]
- cell "deepseek-v4-flash / neuralwatt" [ref=e349]
- cell "$0.0038" [ref=e350]
- cell "1.00" [ref=e351]
- row [ref=e352]:
- cell "03:20:52 AM" [ref=e353]
- cell "chat" [ref=e354]
- cell "tool_use_agentic" [ref=e356]
- cell "3" [ref=e357]
- cell "kimi-k2.7-code / neuralwatt" [ref=e358]
- cell "$0.0185" [ref=e359]
- cell "1.00" [ref=e360]
- row [ref=e361]:
- cell "03:21:02 AM" [ref=e362]
- cell "chat" [ref=e363]
- cell "tool_use_agentic" [ref=e365]
- cell "3" [ref=e366]
- cell "kimi-k2.7-code / neuralwatt" [ref=e367]
- cell "$0.0185" [ref=e368]
- cell "1.00" [ref=e369]
- row [ref=e370]:
- cell "03:21:13 AM" [ref=e371]
- cell "chat" [ref=e372]
- cell "general_chat" [ref=e374]
- cell "2" [ref=e375]
- cell "deepseek-v4-flash / neuralwatt" [ref=e376]
- cell "$0.0039" [ref=e377]
- cell "1.00" [ref=e378]
- row [ref=e379]:
- cell "03:21:18 AM" [ref=e380]
- cell "chat" [ref=e381]
- cell "general_chat" [ref=e383]
- cell "2" [ref=e384]
- cell "deepseek-v4-flash / neuralwatt" [ref=e385]
- cell "$0.0039" [ref=e386]
- cell "1.00" [ref=e387]
- row [ref=e388]:
- cell "03:21:22 AM" [ref=e389]
- cell "chat" [ref=e390]
- cell "general_chat" [ref=e392]
- cell "2" [ref=e393]
- cell "deepseek-v4-flash / neuralwatt" [ref=e394]
- cell "$0.0040" [ref=e395]
- cell "1.00" [ref=e396]
- row [ref=e397]:
- cell "03:21:26 AM" [ref=e398]
- cell "chat" [ref=e399]
- cell "general_chat" [ref=e401]
- cell "2" [ref=e402]
- cell "deepseek-v4-flash / neuralwatt" [ref=e403]
- cell "$0.0040" [ref=e404]
- cell "1.00" [ref=e405]
- row [ref=e406]:
- cell "03:21:30 AM" [ref=e407]
- cell "chat" [ref=e408]
- cell "general_chat" [ref=e410]
- cell "2" [ref=e411]
- cell "deepseek-v4-flash / neuralwatt" [ref=e412]
- cell "$0.0040" [ref=e413]
- cell "1.00" [ref=e414]
- row [ref=e415]:
- cell "03:21:34 AM" [ref=e416]
- cell "chat" [ref=e417]
- cell "general_chat" [ref=e419]
- cell "2" [ref=e420]
- cell "deepseek-v4-flash / neuralwatt" [ref=e421]
- cell "$0.0040" [ref=e422]
- cell "1.00" [ref=e423]
- row [ref=e424]:
- cell "03:21:38 AM" [ref=e425]
- cell "chat" [ref=e426]
- cell "general_chat" [ref=e428]
- cell "2" [ref=e429]
- cell "deepseek-v4-flash / neuralwatt" [ref=e430]
- cell "$0.0040" [ref=e431]
- cell "1.00" [ref=e432]
- row [ref=e433]:
- cell "03:21:55 AM" [ref=e434]
- cell "chat" [ref=e435]
- cell "general_chat" [ref=e437]
- cell "2" [ref=e438]
- cell "deepseek-v4-flash / neuralwatt" [ref=e439]
- cell "$0.0041" [ref=e440]
- cell "1.00" [ref=e441]
- row [ref=e442]:
- cell "03:22:00 AM" [ref=e443]
- cell "chat" [ref=e444]
- cell "general_chat" [ref=e446]
- cell "2" [ref=e447]
- cell "deepseek-v4-flash / neuralwatt" [ref=e448]
- cell "$0.0041" [ref=e449]
- cell "1.00" [ref=e450]
- row [ref=e451]:
- cell "03:23:11 AM" [ref=e452]
- cell "chat" [ref=e453]
- cell "tool_use_agentic" [ref=e455]
- cell "3" [ref=e456]
- cell "kimi-k2.7-code / neuralwatt" [ref=e457]
- cell "$0.0195" [ref=e458]
- cell "1.00" [ref=e459]
- row [ref=e460]:
- cell "03:23:16 AM" [ref=e461]
- cell "chat" [ref=e462]
- cell "general_chat" [ref=e464]
- cell "2" [ref=e465]
- cell "deepseek-v4-flash / neuralwatt" [ref=e466]
- cell "$0.0041" [ref=e467]
- cell "1.00" [ref=e468]
- row [ref=e469]:
- cell "03:24:43 AM" [ref=e470]
- cell "chat" [ref=e471]
- cell "general_chat" [ref=e473]
- cell "2" [ref=e474]
- cell "deepseek-v4-flash / neuralwatt" [ref=e475]
- cell "$0.0041" [ref=e476]
- cell "1.00" [ref=e477]
- row [ref=e478]:
- cell "03:24:54 AM" [ref=e479]
- cell "chat" [ref=e480]
- cell "general_chat" [ref=e482]
- cell "1" [ref=e483]
- cell "gemma-4-31b / neuralwatt" [ref=e484]
- cell "$0.0047" [ref=e485]
- cell "1.00" [ref=e486]
- row [ref=e487]:
- cell "10:40:21 AM" [ref=e488]
- cell "chat" [ref=e489]
- cell "docs_writing" [ref=e491]
- cell "2" [ref=e492]
- cell "kimi-k2.7-code / neuralwatt" [ref=e493]
- cell "$0.0320" [ref=e494]
- cell "0.89" [ref=e495]
- row [ref=e496]:
- cell "10:40:40 AM" [ref=e497]
- cell "chat" [ref=e498]
- cell "docs_writing" [ref=e500]
- cell "2" [ref=e501]
- cell "kimi-k2.7-code / neuralwatt" [ref=e502]
- cell "$0.0326" [ref=e503]
- cell "0.89" [ref=e504]
- row [ref=e505]:
- cell "10:41:02 AM" [ref=e506]
- cell "chat" [ref=e507]
- cell "coding_refactor" [ref=e509]
- cell "2" [ref=e510]
- cell "deepseek-v4-flash / neuralwatt" [ref=e511]
- cell "$0.0017" [ref=e512]
- cell "1.00" [ref=e513]
- row [ref=e514]:
- cell "10:41:08 AM" [ref=e515]
- cell "chat" [ref=e516]
- cell "coding_refactor" [ref=e518]
- cell "2" [ref=e519]
- cell "deepseek-v4-flash / neuralwatt" [ref=e520]
- cell "$0.0018" [ref=e521]
- cell "1.00" [ref=e522]
- row [ref=e523]:
- cell "10:41:11 AM" [ref=e524]
- cell "chat" [ref=e525]
- cell "coding_refactor" [ref=e527]
- cell "2" [ref=e528]
- cell "deepseek-v4-flash / neuralwatt" [ref=e529]
- cell "$0.0019" [ref=e530]
- cell "1.00" [ref=e531]
- row [ref=e532]:
- cell "10:41:15 AM" [ref=e533]
- cell "chat" [ref=e534]
- cell "coding_refactor" [ref=e536]
- cell "2" [ref=e537]
- cell "deepseek-v4-flash / neuralwatt" [ref=e538]
- cell "$0.0019" [ref=e539]
- cell "1.00" [ref=e540]
- row [ref=e541]:
- cell "10:41:20 AM" [ref=e542]
- cell "chat" [ref=e543]
- cell "coding_refactor" [ref=e545]
- cell "2" [ref=e546]
- cell "deepseek-v4-flash / neuralwatt" [ref=e547]
- cell "$0.0019" [ref=e548]
- cell "1.00" [ref=e549]
- row [ref=e550]:
- cell "10:41:24 AM" [ref=e551]
- cell "chat" [ref=e552]
- cell "coding_refactor" [ref=e554]
- cell "2" [ref=e555]
- cell "deepseek-v4-flash / neuralwatt" [ref=e556]
- cell "$0.0019" [ref=e557]
- cell "1.00" [ref=e558]
- row [ref=e559]:
- cell "10:41:28 AM" [ref=e560]
- cell "chat" [ref=e561]
- cell "coding_refactor" [ref=e563]
- cell "2" [ref=e564]
- cell "deepseek-v4-flash / neuralwatt" [ref=e565]
- cell "$0.0019" [ref=e566]
- cell "1.00" [ref=e567]
- row [ref=e568]:
- cell "10:41:33 AM" [ref=e569]
- cell "chat" [ref=e570]
- cell "coding_refactor" [ref=e572]
- cell "2" [ref=e573]
- cell "deepseek-v4-flash / neuralwatt" [ref=e574]
- cell "$0.0020" [ref=e575]
- cell "1.00" [ref=e576]
- row [ref=e577]:
- cell "10:41:37 AM" [ref=e578]
- cell "chat" [ref=e579]
- cell "coding_refactor" [ref=e581]
- cell "2" [ref=e582]
- cell "deepseek-v4-flash / neuralwatt" [ref=e583]
- cell "$0.0020" [ref=e584]
- cell "1.00" [ref=e585]
- row [ref=e586]:
- cell "10:41:43 AM" [ref=e587]
- cell "chat" [ref=e588]
- cell "coding_refactor" [ref=e590]
- cell "2" [ref=e591]
- cell "deepseek-v4-flash / neuralwatt" [ref=e592]
- cell "$0.0020" [ref=e593]
- cell "1.00" [ref=e594]
- row [ref=e595]:
- cell "10:41:47 AM" [ref=e596]
- cell "chat" [ref=e597]
- cell "coding_refactor" [ref=e599]
- cell "2" [ref=e600]
- cell "deepseek-v4-flash / neuralwatt" [ref=e601]
- cell "$0.0020" [ref=e602]
- cell "1.00" [ref=e603]
- row [ref=e604]:
- cell "10:41:51 AM" [ref=e605]
- cell "chat" [ref=e606]
- cell "coding_refactor" [ref=e608]
- cell "2" [ref=e609]
- cell "deepseek-v4-flash / neuralwatt" [ref=e610]
- cell "$0.0020" [ref=e611]
- cell "1.00" [ref=e612]
- row [ref=e613]:
- cell "10:41:55 AM" [ref=e614]
- cell "chat" [ref=e615]
- cell "coding_refactor" [ref=e617]
- cell "2" [ref=e618]
- cell "deepseek-v4-flash / neuralwatt" [ref=e619]
- cell "$0.0021" [ref=e620]
- cell "1.00" [ref=e621]
- row [ref=e622]:
- cell "10:42:05 AM" [ref=e623]
- cell "chat" [ref=e624]
- cell "coding_refactor" [ref=e626]
- cell "2" [ref=e627]
- cell "deepseek-v4-flash / neuralwatt" [ref=e628]
- cell "$0.0021" [ref=e629]
- cell "1.00" [ref=e630]
- row [ref=e631]:
- cell "10:42:11 AM" [ref=e632]
- cell "chat" [ref=e633]
- cell "coding_refactor" [ref=e635]
- cell "2" [ref=e636]
- cell "deepseek-v4-flash / neuralwatt" [ref=e637]
- cell "$0.0021" [ref=e638]
- cell "1.00" [ref=e639]
- row [ref=e640]:
- cell "10:42:16 AM" [ref=e641]
- cell "chat" [ref=e642]
- cell "coding_refactor" [ref=e644]
- cell "2" [ref=e645]
- cell "deepseek-v4-flash / neuralwatt" [ref=e646]
- cell "$0.0023" [ref=e647]
- cell "1.00" [ref=e648]
- row [ref=e649]:
- cell "10:42:21 AM" [ref=e650]
- cell "chat" [ref=e651]
- cell "coding_refactor" [ref=e653]
- cell "2" [ref=e654]
- cell "deepseek-v4-flash / neuralwatt" [ref=e655]
- cell "$0.0023" [ref=e656]
- cell "1.00" [ref=e657]
- row [ref=e658]:
- cell "10:42:25 AM" [ref=e659]
- cell "chat" [ref=e660]
- cell "coding_refactor" [ref=e662]
- cell "2" [ref=e663]
- cell "deepseek-v4-flash / neuralwatt" [ref=e664]
- cell "$0.0023" [ref=e665]
- cell "1.00" [ref=e666]
- generic [ref=e45]:
- heading "▐ Per-Model Usage" [level=2] [ref=e46]:
- generic [ref=e47]: ▐
- text: Per-Model Usage
- generic [ref=e48]:
- generic [ref=e667]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e668]
- generic [ref=e669]: "4335"
- generic [ref=e671]: $8.3957 · 1.11232kWh
- generic [ref=e672]:
- generic "qwen3.6-35b / neuralwatt" [ref=e673]
- generic [ref=e674]: "1440"
- generic [ref=e676]: $1.8333 · 0.22921kWh
- generic [ref=e677]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e678]
- generic [ref=e679]: "907"
- generic [ref=e681]: $5.3526 · 0.66935kWh
- generic [ref=e682]:
- generic "gemma-4-31b / neuralwatt" [ref=e683]
- generic [ref=e684]: "384"
- generic [ref=e686]: $1.1055 · 0.16247kWh
- generic [ref=e687]:
- generic "glm-5.2-fast / neuralwatt" [ref=e688]
- generic [ref=e689]: "358"
- generic [ref=e691]: $4.9423 · 0.61844kWh
- generic [ref=e692]:
- generic "kimi-k3 / neuralwatt" [ref=e693]
- generic [ref=e694]: "151"
- generic [ref=e696]: $3.1220 · 0.39025kWh
- generic [ref=e697]:
- generic "glm-5.2-flex / neuralwatt" [ref=e698]
- generic [ref=e699]: "130"
- generic [ref=e701]: $0.6407 · 0.12087kWh
- generic [ref=e702]:
- generic "kimi-k3-fast / neuralwatt" [ref=e703]
- generic [ref=e704]: "99"
- generic [ref=e706]: $0.5215 · 0.06518kWh
- generic [ref=e707]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e708]
- generic [ref=e709]: "88"
- generic [ref=e711]: $0.0130 · 0.00325kWh
- generic [ref=e712]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e713]
- generic [ref=e714]: "88"
- generic [ref=e716]: $0.0807 · 0.01008kWh
- generic [ref=e717]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e718]
- generic [ref=e719]: "88"
- generic [ref=e721]: $0.0946 · 0.01183kWh
- generic [ref=e722]:
- generic "kimi-k3-flex / neuralwatt" [ref=e723]
- generic [ref=e724]: "88"
- generic [ref=e726]: $0.2680 · 0.03350kWh
- generic [ref=e50]:
- generic [ref=e51]:
- heading "◉ Verdict Mix" [level=2] [ref=e52]:
- generic [ref=e53]: ◉
- text: Verdict Mix
- generic [ref=e727]:
- generic [ref=e728]: "failed: 114"
- generic [ref=e730]: "malformed: 51"
- generic [ref=e732]: "ok: 181"
- generic [ref=e734]: "succeeded: 538"
- generic [ref=e736]: "truncated: 9"
- generic [ref=e738]: "unverifiable: 6887"
- generic [ref=e56]:
- heading "◆ Category Breakdown" [level=2] [ref=e57]:
- generic [ref=e58]: ◆
- text: Category Breakdown
- generic [ref=e59]:
- generic [ref=e740]:
- generic [ref=e742]: coding_refactor
- generic [ref=e743]: "18"
- generic [ref=e744]:
- generic [ref=e746]: general_chat
- generic [ref=e747]: "18"
- generic [ref=e748]:
- generic [ref=e750]: tool_use_agentic
- generic [ref=e751]: "12"
- generic [ref=e752]:
- generic [ref=e754]: docs_writing
- generic [ref=e755]: "2"
- generic [ref=e61]:
- heading "⚠ Warnings" [level=2] [ref=e62]:
- generic [ref=e63]: ⚠
- text: Warnings
- generic [ref=e756]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e66]:
- generic [ref=e67]: ◈
- text: History
- generic [ref=e68]:
- button "1h" [ref=e69] [cursor=pointer]
- button "6h" [ref=e70] [cursor=pointer]
- button "24h" [ref=e71] [cursor=pointer]
- button "7d" [ref=e72] [cursor=pointer]
- button "30d" [ref=e73] [cursor=pointer]
- generic [ref=e76]:
- heading "⚙ Controls" [level=2] [ref=e77]:
- generic [ref=e78]: ⚙
- text: Controls
- generic [ref=e79]:
- generic [ref=e80]:
- text: Operational Triggers
- generic [ref=e81]: — fire-and-forget maintenance jobs
- generic [ref=e82]:
- button "↻ Refresh Catalog" [ref=e83] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e84] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e85] [cursor=pointer]
- button "⟳ Restart Service" [ref=e86] [cursor=pointer]
- generic [ref=e88]:
- generic [ref=e89]:
- text: Runtime Knobs
- generic [ref=e90]: — toggle in-memory; no config.yaml write
- generic [ref=e758]:
- generic [ref=e759]:
- generic [ref=e760]:
- checkbox [checked] [ref=e761]
- generic [ref=e762] [cursor=pointer]
- generic [ref=e763]: log_route_decisions
- generic [ref=e764]: "true"
- generic [ref=e765]:
- generic [ref=e766]:
- checkbox [checked] [ref=e767]
- generic [ref=e768] [cursor=pointer]
- generic [ref=e769]: log_energy_observations
- generic [ref=e770]: "true"
- generic [ref=e771]:
- generic [ref=e772]: circuit_breaker
- generic [ref=e773]: "false"
- generic [ref=e774]:
- generic [ref=e775]:
- checkbox [checked] [ref=e776]
- generic [ref=e777] [cursor=pointer]
- generic [ref=e778]: local_llm_enabled
- generic [ref=e779]: "true"
- generic [ref=e780]:
- generic [ref=e781]:
- checkbox [ref=e782]
- generic [ref=e783] [cursor=pointer]
- generic [ref=e784]: session_cache_enabled
- generic [ref=e785]: "false"
- generic [ref=e786]:
- generic [ref=e787]:
- checkbox [ref=e788]
- generic [ref=e789] [cursor=pointer]
- generic [ref=e790]: pinch_enabled
- generic [ref=e791]: "false"
- generic [ref=e792]:
- generic [ref=e793]:
- checkbox [ref=e794]
- generic [ref=e795] [cursor=pointer]
- generic [ref=e796]: pinch_relevance_enabled
- generic [ref=e797]: "false"
- generic [ref=e798]:
- generic [ref=e799]: default_flex_preference
- textbox [ref=e800]: auto
- generic [ref=e801]: auto
- generic [ref=e91]:
- generic [ref=e92]:
- text: Persisted Config
- generic [ref=e93]: — allowlisted keys only; persisted to config.yaml
- table [ref=e94]:
- rowgroup [ref=e802]:
- row [ref=e803]:
- cell "logging.level" [ref=e804]
- cell [ref=e805]:
- textbox [ref=e806]: info
- cell "info" [ref=e807]
- row [ref=e808]:
- cell "objective.quality_tolerance" [ref=e809]
- cell [ref=e810]:
- textbox [ref=e811]: "0.1"
- cell "0.1" [ref=e812]
- row [ref=e813]:
- cell "objective.max_energy_per_request" [ref=e814]
- cell [ref=e815]:
- textbox [ref=e816]: "null"
- cell "null" [ref=e817]
- row [ref=e818]:
- cell "objective.plan_kwh_per_period" [ref=e819]
- cell [ref=e820]:
- textbox [ref=e821]: "6.25"
- cell "6.25" [ref=e822]
- row [ref=e823]:
- cell "circuit_breaker.enabled" [ref=e824]
- cell [ref=e825]:
- checkbox [ref=e826]
- cell "false" [ref=e827]
- row [ref=e828]:
- cell "session_cache.enabled" [ref=e829]
- cell [ref=e830]:
- checkbox [ref=e831]
- cell "false" [ref=e832]
- row [ref=e833]:
- cell "verification.local_llm_enabled" [ref=e834]
- cell [ref=e835]:
- checkbox [checked] [ref=e836]
- cell "true" [ref=e837]
- row [ref=e838]:
- cell "pinch.enabled" [ref=e839]
- cell [ref=e840]:
- checkbox [ref=e841]
- cell "false" [ref=e842]
- row [ref=e843]:
- cell "pinch.relevance.enabled" [ref=e844]
- cell [ref=e845]:
- checkbox [ref=e846]
- cell "false" [ref=e847]
- row [ref=e848]:
- cell "routing.default_flex_preference" [ref=e849]
- cell [ref=e850]:
- textbox [ref=e851]: auto
- cell "auto" [ref=e852]
- button "Save All Config" [ref=e98] [cursor=pointer]

View File

@@ -0,0 +1,812 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 10:43:48 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e853]:
- generic [ref=e854]:
- generic [ref=e855]: 54.9%
- generic [ref=e857]: 54.9%
- generic [ref=e858]: 8261 calls (30d)
- generic [ref=e859]:
- generic [ref=e860]: Plan
- generic [ref=e861]: 6.25 kWh
- generic [ref=e862]: Metered (30d)
- generic [ref=e863]: 3.4318 kWh
- generic [ref=e864]: Calls (30d)
- generic [ref=e865]: "8261"
- generic [ref=e866]: Resets
- generic [ref=e867]: 2026-07-30
- generic [ref=e868]: Note
- generic [ref=e869]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e15]:
- heading "◈ Model Availability" [level=2] [ref=e16]:
- generic [ref=e17]: ◈
- text: Model Availability
- table [ref=e19]:
- rowgroup [ref=e20]:
- row [ref=e21]:
- columnheader "model" [ref=e22]
- columnheader "provider" [ref=e23]
- columnheader "tier" [ref=e24]
- columnheader "status" [ref=e25]
- columnheader "override" [ref=e26]
- rowgroup [ref=e27]:
- row [ref=e870]:
- cell "deepseek-v4-flash" [ref=e871]
- cell "neuralwatt" [ref=e872]
- cell "2" [ref=e873]
- cell "active" [ref=e874]
- cell "active" [ref=e875]:
- combobox [ref=e876]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e877]:
- cell "deepseek-v4-flash-flex" [ref=e878]
- cell "neuralwatt" [ref=e879]
- cell "2" [ref=e880]
- cell "active" [ref=e881]
- cell "active" [ref=e882]:
- combobox [ref=e883]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e884]:
- cell "gemma-4-31b" [ref=e885]
- cell "neuralwatt" [ref=e886]
- cell "1" [ref=e887]
- cell "active" [ref=e888]
- cell "active" [ref=e889]:
- combobox [ref=e890]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e891]:
- cell "glm-5.2-fast" [ref=e892]
- cell "neuralwatt" [ref=e893]
- cell "2" [ref=e894]
- cell "active" [ref=e895]
- cell "active" [ref=e896]:
- combobox [ref=e897]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e898]:
- cell "glm-5.2-flex" [ref=e899]
- cell "neuralwatt" [ref=e900]
- cell "3" [ref=e901]
- cell "active" [ref=e902]
- cell "active" [ref=e903]:
- combobox [ref=e904]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e905]:
- cell "glm-5.3" [ref=e906]
- cell "neuralwatt" [ref=e907]
- cell "3" [ref=e908]
- cell "active" [ref=e909]
- cell "active" [ref=e910]:
- combobox [ref=e911]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e912]:
- cell "kimi-k2.7-code" [ref=e913]
- cell "neuralwatt" [ref=e914]
- cell "3" [ref=e915]
- cell "active" [ref=e916]
- cell "active" [ref=e917]:
- combobox [ref=e918]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e919]:
- cell "kimi-k2.7-code-fast" [ref=e920]
- cell "neuralwatt" [ref=e921]
- cell "2" [ref=e922]
- cell "active" [ref=e923]
- cell "active" [ref=e924]:
- combobox [ref=e925]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e926]:
- cell "kimi-k2.7-code-flex" [ref=e927]
- cell "neuralwatt" [ref=e928]
- cell "3" [ref=e929]
- cell "active" [ref=e930]
- cell "active" [ref=e931]:
- combobox [ref=e932]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e933]:
- cell "kimi-k3" [ref=e934]
- cell "neuralwatt" [ref=e935]
- cell "3" [ref=e936]
- cell "active" [ref=e937]
- cell "active" [ref=e938]:
- combobox [ref=e939]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e940]:
- cell "kimi-k3-fast" [ref=e941]
- cell "neuralwatt" [ref=e942]
- cell "2" [ref=e943]
- cell "active" [ref=e944]
- cell "active" [ref=e945]:
- combobox [ref=e946]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e947]:
- cell "kimi-k3-flex" [ref=e948]
- cell "neuralwatt" [ref=e949]
- cell "3" [ref=e950]
- cell "active" [ref=e951]
- cell "active" [ref=e952]:
- combobox [ref=e953]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e954]:
- cell "qwen3.6-35b" [ref=e955]
- cell "neuralwatt" [ref=e956]
- cell "3" [ref=e957]
- cell "active" [ref=e958]
- cell "active" [ref=e959]:
- combobox [ref=e960]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e961]:
- cell "qwen3.6-35b-fast" [ref=e962]
- cell "neuralwatt" [ref=e963]
- cell "2" [ref=e964]
- cell "active" [ref=e965]
- cell "active" [ref=e966]:
- combobox [ref=e967]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- generic [ref=e30]:
- generic [ref=e31]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e214]:
- generic [ref=e33]: ⟁
- text: Recent Decisions
- generic [ref=e215]: (50)
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "time" [ref=e38]
- columnheader "kind" [ref=e39]
- columnheader "category" [ref=e40]
- columnheader "tier" [ref=e41]
- columnheader "model" [ref=e42]
- columnheader "cost" [ref=e43]
- columnheader "prof" [ref=e44]
- rowgroup [ref=e216]:
- row [ref=e968]:
- cell "03:19:17 AM" [ref=e969]
- cell "chat" [ref=e970]
- cell "tool_use_agentic" [ref=e972]
- cell "3" [ref=e973]
- cell "kimi-k2.7-code / neuralwatt" [ref=e974]
- cell "$0.0179" [ref=e975]
- cell "1.00" [ref=e976]
- row [ref=e977]:
- cell "03:19:22 AM" [ref=e978]
- cell "chat" [ref=e979]
- cell "general_chat" [ref=e981]
- cell "2" [ref=e982]
- cell "deepseek-v4-flash / neuralwatt" [ref=e983]
- cell "$0.0037" [ref=e984]
- cell "1.00" [ref=e985]
- row [ref=e986]:
- cell "03:19:36 AM" [ref=e987]
- cell "chat" [ref=e988]
- cell "general_chat" [ref=e990]
- cell "3" [ref=e991]
- cell "kimi-k2.7-code / neuralwatt" [ref=e992]
- cell "$0.0179" [ref=e993]
- cell "0.80" [ref=e994]
- row [ref=e995]:
- cell "03:20:19 AM" [ref=e996]
- cell "chat" [ref=e997]
- cell "general_chat" [ref=e999]
- cell "2" [ref=e1000]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1001]
- cell "$0.0037" [ref=e1002]
- cell "1.00" [ref=e1003]
- row [ref=e1004]:
- cell "03:20:25 AM" [ref=e1005]
- cell "chat" [ref=e1006]
- cell "general_chat" [ref=e1008]
- cell "2" [ref=e1009]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1010]
- cell "$0.0038" [ref=e1011]
- cell "1.00" [ref=e1012]
- row [ref=e1013]:
- cell "03:20:31 AM" [ref=e1014]
- cell "chat" [ref=e1015]
- cell "tool_use_agentic" [ref=e1017]
- cell "3" [ref=e1018]
- cell "kimi-k2.7-code / neuralwatt" [ref=e1019]
- cell "$0.0184" [ref=e1020]
- cell "1.00" [ref=e1021]
- row [ref=e1022]:
- cell "03:20:38 AM" [ref=e1023]
- cell "chat" [ref=e1024]
- cell "general_chat" [ref=e1026]
- cell "2" [ref=e1027]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1028]
- cell "$0.0038" [ref=e1029]
- cell "1.00" [ref=e1030]
- row [ref=e1031]:
- cell "03:20:47 AM" [ref=e1032]
- cell "chat" [ref=e1033]
- cell "general_chat" [ref=e1035]
- cell "2" [ref=e1036]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1037]
- cell "$0.0038" [ref=e1038]
- cell "1.00" [ref=e1039]
- row [ref=e1040]:
- cell "03:20:52 AM" [ref=e1041]
- cell "chat" [ref=e1042]
- cell "tool_use_agentic" [ref=e1044]
- cell "3" [ref=e1045]
- cell "kimi-k2.7-code / neuralwatt" [ref=e1046]
- cell "$0.0185" [ref=e1047]
- cell "1.00" [ref=e1048]
- row [ref=e1049]:
- cell "03:21:02 AM" [ref=e1050]
- cell "chat" [ref=e1051]
- cell "tool_use_agentic" [ref=e1053]
- cell "3" [ref=e1054]
- cell "kimi-k2.7-code / neuralwatt" [ref=e1055]
- cell "$0.0185" [ref=e1056]
- cell "1.00" [ref=e1057]
- row [ref=e1058]:
- cell "03:21:13 AM" [ref=e1059]
- cell "chat" [ref=e1060]
- cell "general_chat" [ref=e1062]
- cell "2" [ref=e1063]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1064]
- cell "$0.0039" [ref=e1065]
- cell "1.00" [ref=e1066]
- row [ref=e1067]:
- cell "03:21:18 AM" [ref=e1068]
- cell "chat" [ref=e1069]
- cell "general_chat" [ref=e1071]
- cell "2" [ref=e1072]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1073]
- cell "$0.0039" [ref=e1074]
- cell "1.00" [ref=e1075]
- row [ref=e1076]:
- cell "03:21:22 AM" [ref=e1077]
- cell "chat" [ref=e1078]
- cell "general_chat" [ref=e1080]
- cell "2" [ref=e1081]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1082]
- cell "$0.0040" [ref=e1083]
- cell "1.00" [ref=e1084]
- row [ref=e1085]:
- cell "03:21:26 AM" [ref=e1086]
- cell "chat" [ref=e1087]
- cell "general_chat" [ref=e1089]
- cell "2" [ref=e1090]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1091]
- cell "$0.0040" [ref=e1092]
- cell "1.00" [ref=e1093]
- row [ref=e1094]:
- cell "03:21:30 AM" [ref=e1095]
- cell "chat" [ref=e1096]
- cell "general_chat" [ref=e1098]
- cell "2" [ref=e1099]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1100]
- cell "$0.0040" [ref=e1101]
- cell "1.00" [ref=e1102]
- row [ref=e1103]:
- cell "03:21:34 AM" [ref=e1104]
- cell "chat" [ref=e1105]
- cell "general_chat" [ref=e1107]
- cell "2" [ref=e1108]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1109]
- cell "$0.0040" [ref=e1110]
- cell "1.00" [ref=e1111]
- row [ref=e1112]:
- cell "03:21:38 AM" [ref=e1113]
- cell "chat" [ref=e1114]
- cell "general_chat" [ref=e1116]
- cell "2" [ref=e1117]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1118]
- cell "$0.0040" [ref=e1119]
- cell "1.00" [ref=e1120]
- row [ref=e1121]:
- cell "03:21:55 AM" [ref=e1122]
- cell "chat" [ref=e1123]
- cell "general_chat" [ref=e1125]
- cell "2" [ref=e1126]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1127]
- cell "$0.0041" [ref=e1128]
- cell "1.00" [ref=e1129]
- row [ref=e1130]:
- cell "03:22:00 AM" [ref=e1131]
- cell "chat" [ref=e1132]
- cell "general_chat" [ref=e1134]
- cell "2" [ref=e1135]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1136]
- cell "$0.0041" [ref=e1137]
- cell "1.00" [ref=e1138]
- row [ref=e1139]:
- cell "03:23:11 AM" [ref=e1140]
- cell "chat" [ref=e1141]
- cell "tool_use_agentic" [ref=e1143]
- cell "3" [ref=e1144]
- cell "kimi-k2.7-code / neuralwatt" [ref=e1145]
- cell "$0.0195" [ref=e1146]
- cell "1.00" [ref=e1147]
- row [ref=e1148]:
- cell "03:23:16 AM" [ref=e1149]
- cell "chat" [ref=e1150]
- cell "general_chat" [ref=e1152]
- cell "2" [ref=e1153]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1154]
- cell "$0.0041" [ref=e1155]
- cell "1.00" [ref=e1156]
- row [ref=e1157]:
- cell "03:24:43 AM" [ref=e1158]
- cell "chat" [ref=e1159]
- cell "general_chat" [ref=e1161]
- cell "2" [ref=e1162]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1163]
- cell "$0.0041" [ref=e1164]
- cell "1.00" [ref=e1165]
- row [ref=e1166]:
- cell "03:24:54 AM" [ref=e1167]
- cell "chat" [ref=e1168]
- cell "general_chat" [ref=e1170]
- cell "1" [ref=e1171]
- cell "gemma-4-31b / neuralwatt" [ref=e1172]
- cell "$0.0047" [ref=e1173]
- cell "1.00" [ref=e1174]
- row [ref=e1175]:
- cell "10:40:21 AM" [ref=e1176]
- cell "chat" [ref=e1177]
- cell "docs_writing" [ref=e1179]
- cell "2" [ref=e1180]
- cell "kimi-k2.7-code / neuralwatt" [ref=e1181]
- cell "$0.0320" [ref=e1182]
- cell "0.89" [ref=e1183]
- row [ref=e1184]:
- cell "10:40:40 AM" [ref=e1185]
- cell "chat" [ref=e1186]
- cell "docs_writing" [ref=e1188]
- cell "2" [ref=e1189]
- cell "kimi-k2.7-code / neuralwatt" [ref=e1190]
- cell "$0.0326" [ref=e1191]
- cell "0.89" [ref=e1192]
- row [ref=e1193]:
- cell "10:41:02 AM" [ref=e1194]
- cell "chat" [ref=e1195]
- cell "coding_refactor" [ref=e1197]
- cell "2" [ref=e1198]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1199]
- cell "$0.0017" [ref=e1200]
- cell "1.00" [ref=e1201]
- row [ref=e1202]:
- cell "10:41:08 AM" [ref=e1203]
- cell "chat" [ref=e1204]
- cell "coding_refactor" [ref=e1206]
- cell "2" [ref=e1207]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1208]
- cell "$0.0018" [ref=e1209]
- cell "1.00" [ref=e1210]
- row [ref=e1211]:
- cell "10:41:11 AM" [ref=e1212]
- cell "chat" [ref=e1213]
- cell "coding_refactor" [ref=e1215]
- cell "2" [ref=e1216]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1217]
- cell "$0.0019" [ref=e1218]
- cell "1.00" [ref=e1219]
- row [ref=e1220]:
- cell "10:41:15 AM" [ref=e1221]
- cell "chat" [ref=e1222]
- cell "coding_refactor" [ref=e1224]
- cell "2" [ref=e1225]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1226]
- cell "$0.0019" [ref=e1227]
- cell "1.00" [ref=e1228]
- row [ref=e1229]:
- cell "10:41:20 AM" [ref=e1230]
- cell "chat" [ref=e1231]
- cell "coding_refactor" [ref=e1233]
- cell "2" [ref=e1234]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1235]
- cell "$0.0019" [ref=e1236]
- cell "1.00" [ref=e1237]
- row [ref=e1238]:
- cell "10:41:24 AM" [ref=e1239]
- cell "chat" [ref=e1240]
- cell "coding_refactor" [ref=e1242]
- cell "2" [ref=e1243]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1244]
- cell "$0.0019" [ref=e1245]
- cell "1.00" [ref=e1246]
- row [ref=e1247]:
- cell "10:41:28 AM" [ref=e1248]
- cell "chat" [ref=e1249]
- cell "coding_refactor" [ref=e1251]
- cell "2" [ref=e1252]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1253]
- cell "$0.0019" [ref=e1254]
- cell "1.00" [ref=e1255]
- row [ref=e1256]:
- cell "10:41:33 AM" [ref=e1257]
- cell "chat" [ref=e1258]
- cell "coding_refactor" [ref=e1260]
- cell "2" [ref=e1261]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1262]
- cell "$0.0020" [ref=e1263]
- cell "1.00" [ref=e1264]
- row [ref=e1265]:
- cell "10:41:37 AM" [ref=e1266]
- cell "chat" [ref=e1267]
- cell "coding_refactor" [ref=e1269]
- cell "2" [ref=e1270]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1271]
- cell "$0.0020" [ref=e1272]
- cell "1.00" [ref=e1273]
- row [ref=e1274]:
- cell "10:41:43 AM" [ref=e1275]
- cell "chat" [ref=e1276]
- cell "coding_refactor" [ref=e1278]
- cell "2" [ref=e1279]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1280]
- cell "$0.0020" [ref=e1281]
- cell "1.00" [ref=e1282]
- row [ref=e1283]:
- cell "10:41:47 AM" [ref=e1284]
- cell "chat" [ref=e1285]
- cell "coding_refactor" [ref=e1287]
- cell "2" [ref=e1288]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1289]
- cell "$0.0020" [ref=e1290]
- cell "1.00" [ref=e1291]
- row [ref=e1292]:
- cell "10:41:51 AM" [ref=e1293]
- cell "chat" [ref=e1294]
- cell "coding_refactor" [ref=e1296]
- cell "2" [ref=e1297]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1298]
- cell "$0.0020" [ref=e1299]
- cell "1.00" [ref=e1300]
- row [ref=e1301]:
- cell "10:41:55 AM" [ref=e1302]
- cell "chat" [ref=e1303]
- cell "coding_refactor" [ref=e1305]
- cell "2" [ref=e1306]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1307]
- cell "$0.0021" [ref=e1308]
- cell "1.00" [ref=e1309]
- row [ref=e1310]:
- cell "10:42:05 AM" [ref=e1311]
- cell "chat" [ref=e1312]
- cell "coding_refactor" [ref=e1314]
- cell "2" [ref=e1315]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1316]
- cell "$0.0021" [ref=e1317]
- cell "1.00" [ref=e1318]
- row [ref=e1319]:
- cell "10:42:11 AM" [ref=e1320]
- cell "chat" [ref=e1321]
- cell "coding_refactor" [ref=e1323]
- cell "2" [ref=e1324]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1325]
- cell "$0.0021" [ref=e1326]
- cell "1.00" [ref=e1327]
- row [ref=e1328]:
- cell "10:42:16 AM" [ref=e1329]
- cell "chat" [ref=e1330]
- cell "coding_refactor" [ref=e1332]
- cell "2" [ref=e1333]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1334]
- cell "$0.0023" [ref=e1335]
- cell "1.00" [ref=e1336]
- row [ref=e1337]:
- cell "10:42:21 AM" [ref=e1338]
- cell "chat" [ref=e1339]
- cell "coding_refactor" [ref=e1341]
- cell "2" [ref=e1342]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1343]
- cell "$0.0023" [ref=e1344]
- cell "1.00" [ref=e1345]
- row [ref=e1346]:
- cell "10:42:25 AM" [ref=e1347]
- cell "chat" [ref=e1348]
- cell "coding_refactor" [ref=e1350]
- cell "2" [ref=e1351]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1352]
- cell "$0.0023" [ref=e1353]
- cell "1.00" [ref=e1354]
- row [ref=e1355]:
- cell "10:43:00 AM" [ref=e1356]
- cell "chat" [ref=e1357]
- cell "coding_refactor" [ref=e1359]
- cell "2" [ref=e1360]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1361]
- cell "$0.0023" [ref=e1362]
- cell "1.00" [ref=e1363]
- row [ref=e1364]:
- cell "10:43:04 AM" [ref=e1365]
- cell "chat" [ref=e1366]
- cell "coding_refactor" [ref=e1368]
- cell "2" [ref=e1369]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1370]
- cell "$0.0023" [ref=e1371]
- cell "1.00" [ref=e1372]
- row [ref=e1373]:
- cell "10:43:09 AM" [ref=e1374]
- cell "chat" [ref=e1375]
- cell "coding_refactor" [ref=e1377]
- cell "2" [ref=e1378]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1379]
- cell "$0.0023" [ref=e1380]
- cell "1.00" [ref=e1381]
- row [ref=e1382]:
- cell "10:43:13 AM" [ref=e1383]
- cell "chat" [ref=e1384]
- cell "coding_refactor" [ref=e1386]
- cell "2" [ref=e1387]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1388]
- cell "$0.0023" [ref=e1389]
- cell "1.00" [ref=e1390]
- row [ref=e1391]:
- cell "10:43:19 AM" [ref=e1392]
- cell "chat" [ref=e1393]
- cell "coding_refactor" [ref=e1395]
- cell "2" [ref=e1396]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1397]
- cell "$0.0024" [ref=e1398]
- cell "1.00" [ref=e1399]
- row [ref=e1400]:
- cell "10:43:25 AM" [ref=e1401]
- cell "chat" [ref=e1402]
- cell "coding_refactor" [ref=e1404]
- cell "2" [ref=e1405]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1406]
- cell "$0.0024" [ref=e1407]
- cell "1.00" [ref=e1408]
- row [ref=e1409]:
- cell "10:43:31 AM" [ref=e1410]
- cell "chat" [ref=e1411]
- cell "coding_refactor" [ref=e1413]
- cell "2" [ref=e1414]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1415]
- cell "$0.0024" [ref=e1416]
- cell "1.00" [ref=e1417]
- generic [ref=e45]:
- heading "▐ Per-Model Usage" [level=2] [ref=e46]:
- generic [ref=e47]: ▐
- text: Per-Model Usage
- generic [ref=e48]:
- generic [ref=e1418]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e1419]
- generic [ref=e1420]: "4342"
- generic [ref=e1422]: $8.4007 · 1.11294kWh
- generic [ref=e1423]:
- generic "qwen3.6-35b / neuralwatt" [ref=e1424]
- generic [ref=e1425]: "1440"
- generic [ref=e1427]: $1.8333 · 0.22921kWh
- generic [ref=e1428]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e1429]
- generic [ref=e1430]: "907"
- generic [ref=e1432]: $5.3526 · 0.66935kWh
- generic [ref=e1433]:
- generic "gemma-4-31b / neuralwatt" [ref=e1434]
- generic [ref=e1435]: "384"
- generic [ref=e1437]: $1.1055 · 0.16247kWh
- generic [ref=e1438]:
- generic "glm-5.2-fast / neuralwatt" [ref=e1439]
- generic [ref=e1440]: "358"
- generic [ref=e1442]: $4.9423 · 0.61844kWh
- generic [ref=e1443]:
- generic "kimi-k3 / neuralwatt" [ref=e1444]
- generic [ref=e1445]: "151"
- generic [ref=e1447]: $3.1220 · 0.39025kWh
- generic [ref=e1448]:
- generic "glm-5.2-flex / neuralwatt" [ref=e1449]
- generic [ref=e1450]: "130"
- generic [ref=e1452]: $0.6407 · 0.12087kWh
- generic [ref=e1453]:
- generic "kimi-k3-fast / neuralwatt" [ref=e1454]
- generic [ref=e1455]: "99"
- generic [ref=e1457]: $0.5215 · 0.06518kWh
- generic [ref=e1458]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e1459]
- generic [ref=e1460]: "88"
- generic [ref=e1462]: $0.0130 · 0.00325kWh
- generic [ref=e1463]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e1464]
- generic [ref=e1465]: "88"
- generic [ref=e1467]: $0.0807 · 0.01008kWh
- generic [ref=e1468]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e1469]
- generic [ref=e1470]: "88"
- generic [ref=e1472]: $0.0946 · 0.01183kWh
- generic [ref=e1473]:
- generic "kimi-k3-flex / neuralwatt" [ref=e1474]
- generic [ref=e1475]: "88"
- generic [ref=e1477]: $0.2680 · 0.03350kWh
- generic [ref=e50]:
- generic [ref=e51]:
- heading "◉ Verdict Mix" [level=2] [ref=e52]:
- generic [ref=e53]: ◉
- text: Verdict Mix
- generic [ref=e727]:
- generic [ref=e1478]: "failed: 114"
- generic [ref=e1480]: "malformed: 51"
- generic [ref=e1482]: "ok: 181"
- generic [ref=e1484]: "succeeded: 538"
- generic [ref=e1486]: "truncated: 9"
- generic [ref=e1488]: "unverifiable: 6894"
- generic [ref=e56]:
- heading "◆ Category Breakdown" [level=2] [ref=e57]:
- generic [ref=e58]: ◆
- text: Category Breakdown
- generic [ref=e59]:
- generic [ref=e1490]:
- generic [ref=e1492]: coding_refactor
- generic [ref=e1493]: "25"
- generic [ref=e1494]:
- generic [ref=e1496]: general_chat
- generic [ref=e1497]: "18"
- generic [ref=e1498]:
- generic [ref=e1500]: tool_use_agentic
- generic [ref=e1501]: "5"
- generic [ref=e1502]:
- generic [ref=e1504]: docs_writing
- generic [ref=e1505]: "2"
- generic [ref=e61]:
- heading "⚠ Warnings" [level=2] [ref=e62]:
- generic [ref=e63]: ⚠
- text: Warnings
- generic [ref=e756]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e66]:
- generic [ref=e67]: ◈
- text: History
- generic [ref=e68]:
- button "1h" [ref=e69] [cursor=pointer]
- button "6h" [ref=e70] [cursor=pointer]
- button "24h" [ref=e71] [cursor=pointer]
- button "7d" [ref=e72] [cursor=pointer]
- button "30d" [ref=e73] [cursor=pointer]
- generic [ref=e76]:
- heading "⚙ Controls" [level=2] [ref=e77]:
- generic [ref=e78]: ⚙
- text: Controls
- generic [ref=e79]:
- generic [ref=e80]:
- text: Operational Triggers
- generic [ref=e81]: — fire-and-forget maintenance jobs
- generic [ref=e82]:
- button "↻ Refresh Catalog" [ref=e83] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e84] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e85] [cursor=pointer]
- button "⟳ Restart Service" [ref=e86] [cursor=pointer]
- generic [ref=e88]:
- generic [ref=e89]:
- text: Runtime Knobs
- generic [ref=e90]: — toggle in-memory; no config.yaml write
- generic [ref=e758]:
- generic [ref=e1507]:
- generic [ref=e1508]:
- checkbox [checked] [ref=e1509]
- generic [ref=e1510] [cursor=pointer]
- generic [ref=e1511]: log_route_decisions
- generic [ref=e1512]: "true"
- generic [ref=e1513]:
- generic [ref=e1514]:
- checkbox [checked] [ref=e1515]
- generic [ref=e1516] [cursor=pointer]
- generic [ref=e1517]: log_energy_observations
- generic [ref=e1518]: "true"
- generic [ref=e1519]:
- generic [ref=e1520]: circuit_breaker
- generic [ref=e1521]: "false"
- generic [ref=e1522]:
- generic [ref=e1523]:
- checkbox [checked] [ref=e1524]
- generic [ref=e1525] [cursor=pointer]
- generic [ref=e1526]: local_llm_enabled
- generic [ref=e1527]: "true"
- generic [ref=e1528]:
- generic [ref=e1529]:
- checkbox [ref=e1530]
- generic [ref=e1531] [cursor=pointer]
- generic [ref=e1532]: session_cache_enabled
- generic [ref=e1533]: "false"
- generic [ref=e1534]:
- generic [ref=e1535]:
- checkbox [ref=e1536]
- generic [ref=e1537] [cursor=pointer]
- generic [ref=e1538]: pinch_enabled
- generic [ref=e1539]: "false"
- generic [ref=e1540]:
- generic [ref=e1541]:
- checkbox [ref=e1542]
- generic [ref=e1543] [cursor=pointer]
- generic [ref=e1544]: pinch_relevance_enabled
- generic [ref=e1545]: "false"
- generic [ref=e1546]:
- generic [ref=e1547]: default_flex_preference
- textbox [ref=e1548]: auto
- generic [ref=e1549]: auto
- generic [ref=e91]:
- generic [ref=e92]:
- text: Persisted Config
- generic [ref=e93]: — allowlisted keys only; persisted to config.yaml
- table [ref=e94]:
- rowgroup [ref=e1550]:
- row [ref=e1551]:
- cell "logging.level" [ref=e1552]
- cell [ref=e1553]:
- textbox [ref=e1554]: info
- cell "info" [ref=e1555]
- row [ref=e1556]:
- cell "objective.quality_tolerance" [ref=e1557]
- cell [ref=e1558]:
- textbox [ref=e1559]: "0.1"
- cell "0.1" [ref=e1560]
- row [ref=e1561]:
- cell "objective.max_energy_per_request" [ref=e1562]
- cell [ref=e1563]:
- textbox [ref=e1564]: "null"
- cell "null" [ref=e1565]
- row [ref=e1566]:
- cell "objective.plan_kwh_per_period" [ref=e1567]
- cell [ref=e1568]:
- textbox [ref=e1569]: "6.25"
- cell "6.25" [ref=e1570]
- row [ref=e1571]:
- cell "circuit_breaker.enabled" [ref=e1572]
- cell [ref=e1573]:
- checkbox [ref=e1574]
- cell "false" [ref=e1575]
- row [ref=e1576]:
- cell "session_cache.enabled" [ref=e1577]
- cell [ref=e1578]:
- checkbox [ref=e1579]
- cell "false" [ref=e1580]
- row [ref=e1581]:
- cell "verification.local_llm_enabled" [ref=e1582]
- cell [ref=e1583]:
- checkbox [checked] [ref=e1584]
- cell "true" [ref=e1585]
- row [ref=e1586]:
- cell "pinch.enabled" [ref=e1587]
- cell [ref=e1588]:
- checkbox [ref=e1589]
- cell "false" [ref=e1590]
- row [ref=e1591]:
- cell "pinch.relevance.enabled" [ref=e1592]
- cell [ref=e1593]:
- checkbox [ref=e1594]
- cell "false" [ref=e1595]
- row [ref=e1596]:
- cell "routing.default_flex_preference" [ref=e1597]
- cell [ref=e1598]:
- textbox [ref=e1599]: auto
- cell "auto" [ref=e1600]
- button "Save All Config" [ref=e98] [cursor=pointer]

View File

@@ -0,0 +1,581 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 10:47:56 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: 55.0%
- generic [ref=e18]: 55.0%
- generic [ref=e19]: 8278 calls (30d)
- generic [ref=e20]:
- generic [ref=e21]: Plan
- generic [ref=e22]: 6.25 kWh
- generic [ref=e23]: Metered (30d)
- generic [ref=e24]: 3.43555 kWh
- generic [ref=e25]: Calls (30d)
- generic [ref=e26]: "8278"
- generic [ref=e27]: Resets
- generic [ref=e28]: 2026-07-30
- generic [ref=e29]: Note
- generic [ref=e30]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e31]:
- heading "◈ Model Availability" [level=2] [ref=e32]:
- generic [ref=e33]: ◈
- text: Model Availability
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "model" [ref=e38]
- columnheader "provider" [ref=e39]
- columnheader "tier" [ref=e40]
- columnheader "status" [ref=e41]
- columnheader "override" [ref=e42]
- rowgroup [ref=e43]:
- row [ref=e44]:
- cell "Loading…" [ref=e45]
- generic [ref=e46]:
- generic [ref=e47]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e48]:
- generic [ref=e49]: ⟁
- text: Recent Decisions
- generic [ref=e50]: (50)
- table [ref=e52]:
- rowgroup [ref=e53]:
- row [ref=e54]:
- columnheader "time" [ref=e55]
- columnheader "kind" [ref=e56]
- columnheader "category" [ref=e57]
- columnheader "tier" [ref=e58]
- columnheader "model" [ref=e59]
- columnheader "cost" [ref=e60]
- columnheader "prof" [ref=e61]
- rowgroup [ref=e62]:
- row [ref=e63]:
- cell "03:21:55 AM" [ref=e64]
- cell "chat" [ref=e65]
- cell "general_chat" [ref=e67]
- cell "2" [ref=e68]
- cell "deepseek-v4-flash / neuralwatt" [ref=e69]
- cell "$0.0041" [ref=e70]
- cell "1.00" [ref=e71]
- row [ref=e72]:
- cell "03:22:00 AM" [ref=e73]
- cell "chat" [ref=e74]
- cell "general_chat" [ref=e76]
- cell "2" [ref=e77]
- cell "deepseek-v4-flash / neuralwatt" [ref=e78]
- cell "$0.0041" [ref=e79]
- cell "1.00" [ref=e80]
- row [ref=e81]:
- cell "03:23:11 AM" [ref=e82]
- cell "chat" [ref=e83]
- cell "tool_use_agentic" [ref=e85]
- cell "3" [ref=e86]
- cell "kimi-k2.7-code / neuralwatt" [ref=e87]
- cell "$0.0195" [ref=e88]
- cell "1.00" [ref=e89]
- row [ref=e90]:
- cell "03:23:16 AM" [ref=e91]
- cell "chat" [ref=e92]
- cell "general_chat" [ref=e94]
- cell "2" [ref=e95]
- cell "deepseek-v4-flash / neuralwatt" [ref=e96]
- cell "$0.0041" [ref=e97]
- cell "1.00" [ref=e98]
- row [ref=e99]:
- cell "03:24:43 AM" [ref=e100]
- cell "chat" [ref=e101]
- cell "general_chat" [ref=e103]
- cell "2" [ref=e104]
- cell "deepseek-v4-flash / neuralwatt" [ref=e105]
- cell "$0.0041" [ref=e106]
- cell "1.00" [ref=e107]
- row [ref=e108]:
- cell "03:24:54 AM" [ref=e109]
- cell "chat" [ref=e110]
- cell "general_chat" [ref=e112]
- cell "1" [ref=e113]
- cell "gemma-4-31b / neuralwatt" [ref=e114]
- cell "$0.0047" [ref=e115]
- cell "1.00" [ref=e116]
- row [ref=e117]:
- cell "10:40:21 AM" [ref=e118]
- cell "chat" [ref=e119]
- cell "docs_writing" [ref=e121]
- cell "2" [ref=e122]
- cell "kimi-k2.7-code / neuralwatt" [ref=e123]
- cell "$0.0320" [ref=e124]
- cell "0.89" [ref=e125]
- row [ref=e126]:
- cell "10:40:40 AM" [ref=e127]
- cell "chat" [ref=e128]
- cell "docs_writing" [ref=e130]
- cell "2" [ref=e131]
- cell "kimi-k2.7-code / neuralwatt" [ref=e132]
- cell "$0.0326" [ref=e133]
- cell "0.89" [ref=e134]
- row [ref=e135]:
- cell "10:41:02 AM" [ref=e136]
- cell "chat" [ref=e137]
- cell "coding_refactor" [ref=e139]
- cell "2" [ref=e140]
- cell "deepseek-v4-flash / neuralwatt" [ref=e141]
- cell "$0.0017" [ref=e142]
- cell "1.00" [ref=e143]
- row [ref=e144]:
- cell "10:41:08 AM" [ref=e145]
- cell "chat" [ref=e146]
- cell "coding_refactor" [ref=e148]
- cell "2" [ref=e149]
- cell "deepseek-v4-flash / neuralwatt" [ref=e150]
- cell "$0.0018" [ref=e151]
- cell "1.00" [ref=e152]
- row [ref=e153]:
- cell "10:41:11 AM" [ref=e154]
- cell "chat" [ref=e155]
- cell "coding_refactor" [ref=e157]
- cell "2" [ref=e158]
- cell "deepseek-v4-flash / neuralwatt" [ref=e159]
- cell "$0.0019" [ref=e160]
- cell "1.00" [ref=e161]
- row [ref=e162]:
- cell "10:41:15 AM" [ref=e163]
- cell "chat" [ref=e164]
- cell "coding_refactor" [ref=e166]
- cell "2" [ref=e167]
- cell "deepseek-v4-flash / neuralwatt" [ref=e168]
- cell "$0.0019" [ref=e169]
- cell "1.00" [ref=e170]
- row [ref=e171]:
- cell "10:41:20 AM" [ref=e172]
- cell "chat" [ref=e173]
- cell "coding_refactor" [ref=e175]
- cell "2" [ref=e176]
- cell "deepseek-v4-flash / neuralwatt" [ref=e177]
- cell "$0.0019" [ref=e178]
- cell "1.00" [ref=e179]
- row [ref=e180]:
- cell "10:41:24 AM" [ref=e181]
- cell "chat" [ref=e182]
- cell "coding_refactor" [ref=e184]
- cell "2" [ref=e185]
- cell "deepseek-v4-flash / neuralwatt" [ref=e186]
- cell "$0.0019" [ref=e187]
- cell "1.00" [ref=e188]
- row [ref=e189]:
- cell "10:41:28 AM" [ref=e190]
- cell "chat" [ref=e191]
- cell "coding_refactor" [ref=e193]
- cell "2" [ref=e194]
- cell "deepseek-v4-flash / neuralwatt" [ref=e195]
- cell "$0.0019" [ref=e196]
- cell "1.00" [ref=e197]
- row [ref=e198]:
- cell "10:41:33 AM" [ref=e199]
- cell "chat" [ref=e200]
- cell "coding_refactor" [ref=e202]
- cell "2" [ref=e203]
- cell "deepseek-v4-flash / neuralwatt" [ref=e204]
- cell "$0.0020" [ref=e205]
- cell "1.00" [ref=e206]
- row [ref=e207]:
- cell "10:41:37 AM" [ref=e208]
- cell "chat" [ref=e209]
- cell "coding_refactor" [ref=e211]
- cell "2" [ref=e212]
- cell "deepseek-v4-flash / neuralwatt" [ref=e213]
- cell "$0.0020" [ref=e214]
- cell "1.00" [ref=e215]
- row [ref=e216]:
- cell "10:41:43 AM" [ref=e217]
- cell "chat" [ref=e218]
- cell "coding_refactor" [ref=e220]
- cell "2" [ref=e221]
- cell "deepseek-v4-flash / neuralwatt" [ref=e222]
- cell "$0.0020" [ref=e223]
- cell "1.00" [ref=e224]
- row [ref=e225]:
- cell "10:41:47 AM" [ref=e226]
- cell "chat" [ref=e227]
- cell "coding_refactor" [ref=e229]
- cell "2" [ref=e230]
- cell "deepseek-v4-flash / neuralwatt" [ref=e231]
- cell "$0.0020" [ref=e232]
- cell "1.00" [ref=e233]
- row [ref=e234]:
- cell "10:41:51 AM" [ref=e235]
- cell "chat" [ref=e236]
- cell "coding_refactor" [ref=e238]
- cell "2" [ref=e239]
- cell "deepseek-v4-flash / neuralwatt" [ref=e240]
- cell "$0.0020" [ref=e241]
- cell "1.00" [ref=e242]
- row [ref=e243]:
- cell "10:41:55 AM" [ref=e244]
- cell "chat" [ref=e245]
- cell "coding_refactor" [ref=e247]
- cell "2" [ref=e248]
- cell "deepseek-v4-flash / neuralwatt" [ref=e249]
- cell "$0.0021" [ref=e250]
- cell "1.00" [ref=e251]
- row [ref=e252]:
- cell "10:42:05 AM" [ref=e253]
- cell "chat" [ref=e254]
- cell "coding_refactor" [ref=e256]
- cell "2" [ref=e257]
- cell "deepseek-v4-flash / neuralwatt" [ref=e258]
- cell "$0.0021" [ref=e259]
- cell "1.00" [ref=e260]
- row [ref=e261]:
- cell "10:42:11 AM" [ref=e262]
- cell "chat" [ref=e263]
- cell "coding_refactor" [ref=e265]
- cell "2" [ref=e266]
- cell "deepseek-v4-flash / neuralwatt" [ref=e267]
- cell "$0.0021" [ref=e268]
- cell "1.00" [ref=e269]
- row [ref=e270]:
- cell "10:42:16 AM" [ref=e271]
- cell "chat" [ref=e272]
- cell "coding_refactor" [ref=e274]
- cell "2" [ref=e275]
- cell "deepseek-v4-flash / neuralwatt" [ref=e276]
- cell "$0.0023" [ref=e277]
- cell "1.00" [ref=e278]
- row [ref=e279]:
- cell "10:42:21 AM" [ref=e280]
- cell "chat" [ref=e281]
- cell "coding_refactor" [ref=e283]
- cell "2" [ref=e284]
- cell "deepseek-v4-flash / neuralwatt" [ref=e285]
- cell "$0.0023" [ref=e286]
- cell "1.00" [ref=e287]
- row [ref=e288]:
- cell "10:42:25 AM" [ref=e289]
- cell "chat" [ref=e290]
- cell "coding_refactor" [ref=e292]
- cell "2" [ref=e293]
- cell "deepseek-v4-flash / neuralwatt" [ref=e294]
- cell "$0.0023" [ref=e295]
- cell "1.00" [ref=e296]
- row [ref=e297]:
- cell "10:43:00 AM" [ref=e298]
- cell "chat" [ref=e299]
- cell "coding_refactor" [ref=e301]
- cell "2" [ref=e302]
- cell "deepseek-v4-flash / neuralwatt" [ref=e303]
- cell "$0.0023" [ref=e304]
- cell "1.00" [ref=e305]
- row [ref=e306]:
- cell "10:43:04 AM" [ref=e307]
- cell "chat" [ref=e308]
- cell "coding_refactor" [ref=e310]
- cell "2" [ref=e311]
- cell "deepseek-v4-flash / neuralwatt" [ref=e312]
- cell "$0.0023" [ref=e313]
- cell "1.00" [ref=e314]
- row [ref=e315]:
- cell "10:43:09 AM" [ref=e316]
- cell "chat" [ref=e317]
- cell "coding_refactor" [ref=e319]
- cell "2" [ref=e320]
- cell "deepseek-v4-flash / neuralwatt" [ref=e321]
- cell "$0.0023" [ref=e322]
- cell "1.00" [ref=e323]
- row [ref=e324]:
- cell "10:43:13 AM" [ref=e325]
- cell "chat" [ref=e326]
- cell "coding_refactor" [ref=e328]
- cell "2" [ref=e329]
- cell "deepseek-v4-flash / neuralwatt" [ref=e330]
- cell "$0.0023" [ref=e331]
- cell "1.00" [ref=e332]
- row [ref=e333]:
- cell "10:43:19 AM" [ref=e334]
- cell "chat" [ref=e335]
- cell "coding_refactor" [ref=e337]
- cell "2" [ref=e338]
- cell "deepseek-v4-flash / neuralwatt" [ref=e339]
- cell "$0.0024" [ref=e340]
- cell "1.00" [ref=e341]
- row [ref=e342]:
- cell "10:43:25 AM" [ref=e343]
- cell "chat" [ref=e344]
- cell "coding_refactor" [ref=e346]
- cell "2" [ref=e347]
- cell "deepseek-v4-flash / neuralwatt" [ref=e348]
- cell "$0.0024" [ref=e349]
- cell "1.00" [ref=e350]
- row [ref=e351]:
- cell "10:43:31 AM" [ref=e352]
- cell "chat" [ref=e353]
- cell "coding_refactor" [ref=e355]
- cell "2" [ref=e356]
- cell "deepseek-v4-flash / neuralwatt" [ref=e357]
- cell "$0.0024" [ref=e358]
- cell "1.00" [ref=e359]
- row [ref=e360]:
- cell "10:44:06 AM" [ref=e361]
- cell "chat" [ref=e362]
- cell "coding_refactor" [ref=e364]
- cell "2" [ref=e365]
- cell "deepseek-v4-flash / neuralwatt" [ref=e366]
- cell "$0.0024" [ref=e367]
- cell "1.00" [ref=e368]
- row [ref=e369]:
- cell "10:44:22 AM" [ref=e370]
- cell "chat" [ref=e371]
- cell "coding_refactor" [ref=e373]
- cell "2" [ref=e374]
- cell "deepseek-v4-flash / neuralwatt" [ref=e375]
- cell "$0.0024" [ref=e376]
- cell "1.00" [ref=e377]
- row [ref=e378]:
- cell "10:44:26 AM" [ref=e379]
- cell "chat" [ref=e380]
- cell "coding_refactor" [ref=e382]
- cell "2" [ref=e383]
- cell "deepseek-v4-flash / neuralwatt" [ref=e384]
- cell "$0.0024" [ref=e385]
- cell "1.00" [ref=e386]
- row [ref=e387]:
- cell "10:44:30 AM" [ref=e388]
- cell "chat" [ref=e389]
- cell "coding_refactor" [ref=e391]
- cell "2" [ref=e392]
- cell "deepseek-v4-flash / neuralwatt" [ref=e393]
- cell "$0.0024" [ref=e394]
- cell "1.00" [ref=e395]
- row [ref=e396]:
- cell "10:44:36 AM" [ref=e397]
- cell "chat" [ref=e398]
- cell "coding_refactor" [ref=e400]
- cell "2" [ref=e401]
- cell "deepseek-v4-flash / neuralwatt" [ref=e402]
- cell "$0.0024" [ref=e403]
- cell "1.00" [ref=e404]
- row [ref=e405]:
- cell "10:44:42 AM" [ref=e406]
- cell "chat" [ref=e407]
- cell "coding_refactor" [ref=e409]
- cell "2" [ref=e410]
- cell "deepseek-v4-flash / neuralwatt" [ref=e411]
- cell "$0.0024" [ref=e412]
- cell "1.00" [ref=e413]
- row [ref=e414]:
- cell "10:44:50 AM" [ref=e415]
- cell "chat" [ref=e416]
- cell "coding_refactor" [ref=e418]
- cell "2" [ref=e419]
- cell "deepseek-v4-flash / neuralwatt" [ref=e420]
- cell "$0.0025" [ref=e421]
- cell "1.00" [ref=e422]
- row [ref=e423]:
- cell "10:46:57 AM" [ref=e424]
- cell "chat" [ref=e425]
- cell "coding_refactor" [ref=e427]
- cell "2" [ref=e428]
- cell "deepseek-v4-flash / neuralwatt" [ref=e429]
- cell "$0.0025" [ref=e430]
- cell "1.00" [ref=e431]
- row [ref=e432]:
- cell "10:47:02 AM" [ref=e433]
- cell "chat" [ref=e434]
- cell "coding_refactor" [ref=e436]
- cell "2" [ref=e437]
- cell "deepseek-v4-flash / neuralwatt" [ref=e438]
- cell "$0.0025" [ref=e439]
- cell "1.00" [ref=e440]
- row [ref=e441]:
- cell "10:47:09 AM" [ref=e442]
- cell "chat" [ref=e443]
- cell "coding_refactor" [ref=e445]
- cell "2" [ref=e446]
- cell "deepseek-v4-flash / neuralwatt" [ref=e447]
- cell "$0.0025" [ref=e448]
- cell "1.00" [ref=e449]
- row [ref=e450]:
- cell "10:47:13 AM" [ref=e451]
- cell "chat" [ref=e452]
- cell "coding_refactor" [ref=e454]
- cell "2" [ref=e455]
- cell "deepseek-v4-flash / neuralwatt" [ref=e456]
- cell "$0.0025" [ref=e457]
- cell "1.00" [ref=e458]
- row [ref=e459]:
- cell "10:47:18 AM" [ref=e460]
- cell "chat" [ref=e461]
- cell "tool_use_agentic" [ref=e463]
- cell "2" [ref=e464]
- cell "qwen3.6-35b / neuralwatt" [ref=e465]
- cell "$0.0008" [ref=e466]
- cell "1.00" [ref=e467]
- row [ref=e468]:
- cell "10:47:28 AM" [ref=e469]
- cell "chat" [ref=e470]
- cell "coding_refactor" [ref=e472]
- cell "2" [ref=e473]
- cell "deepseek-v4-flash / neuralwatt" [ref=e474]
- cell "$0.0025" [ref=e475]
- cell "1.00" [ref=e476]
- row [ref=e477]:
- cell "10:47:33 AM" [ref=e478]
- cell "chat" [ref=e479]
- cell "coding_refactor" [ref=e481]
- cell "2" [ref=e482]
- cell "deepseek-v4-flash / neuralwatt" [ref=e483]
- cell "$0.0025" [ref=e484]
- cell "1.00" [ref=e485]
- row [ref=e486]:
- cell "10:47:38 AM" [ref=e487]
- cell "chat" [ref=e488]
- cell "coding_refactor" [ref=e490]
- cell "2" [ref=e491]
- cell "deepseek-v4-flash / neuralwatt" [ref=e492]
- cell "$0.0026" [ref=e493]
- cell "1.00" [ref=e494]
- row [ref=e495]:
- cell "10:47:49 AM" [ref=e496]
- cell "chat" [ref=e497]
- cell "coding_refactor" [ref=e499]
- cell "2" [ref=e500]
- cell "deepseek-v4-flash / neuralwatt" [ref=e501]
- cell "$0.0026" [ref=e502]
- cell "1.00" [ref=e503]
- row [ref=e504]:
- cell "10:47:54 AM" [ref=e505]
- cell "chat" [ref=e506]
- cell "coding_refactor" [ref=e508]
- cell "2" [ref=e509]
- cell "deepseek-v4-flash / neuralwatt" [ref=e510]
- cell "$0.0026" [ref=e511]
- cell "1.00" [ref=e512]
- generic [ref=e513]:
- heading "▐ Per-Model Usage" [level=2] [ref=e514]:
- generic [ref=e515]: ▐
- text: Per-Model Usage
- generic [ref=e516]:
- generic [ref=e517]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e518]
- generic [ref=e519]: "4358"
- generic [ref=e521]: $8.4243 · 1.11649kWh
- generic [ref=e522]:
- generic "qwen3.6-35b / neuralwatt" [ref=e523]
- generic [ref=e524]: "1441"
- generic [ref=e526]: $1.8349 · 0.22940kWh
- generic [ref=e527]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e528]
- generic [ref=e529]: "907"
- generic [ref=e531]: $5.3526 · 0.66935kWh
- generic [ref=e532]:
- generic "gemma-4-31b / neuralwatt" [ref=e533]
- generic [ref=e534]: "384"
- generic [ref=e536]: $1.1055 · 0.16247kWh
- generic [ref=e537]:
- generic "glm-5.2-fast / neuralwatt" [ref=e538]
- generic [ref=e539]: "358"
- generic [ref=e541]: $4.9423 · 0.61844kWh
- generic [ref=e542]:
- generic "kimi-k3 / neuralwatt" [ref=e543]
- generic [ref=e544]: "151"
- generic [ref=e546]: $3.1220 · 0.39025kWh
- generic [ref=e547]:
- generic "glm-5.2-flex / neuralwatt" [ref=e548]
- generic [ref=e549]: "130"
- generic [ref=e551]: $0.6407 · 0.12087kWh
- generic [ref=e552]:
- generic "kimi-k3-fast / neuralwatt" [ref=e553]
- generic [ref=e554]: "99"
- generic [ref=e556]: $0.5215 · 0.06518kWh
- generic [ref=e557]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e558]
- generic [ref=e559]: "88"
- generic [ref=e561]: $0.0130 · 0.00325kWh
- generic [ref=e562]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e563]
- generic [ref=e564]: "88"
- generic [ref=e566]: $0.0807 · 0.01008kWh
- generic [ref=e567]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e568]
- generic [ref=e569]: "88"
- generic [ref=e571]: $0.0946 · 0.01183kWh
- generic [ref=e572]:
- generic "kimi-k3-flex / neuralwatt" [ref=e573]
- generic [ref=e574]: "88"
- generic [ref=e576]: $0.2680 · 0.03350kWh
- generic [ref=e577]:
- generic [ref=e578]:
- heading "◉ Verdict Mix" [level=2] [ref=e579]:
- generic [ref=e580]: ◉
- text: Verdict Mix
- generic [ref=e583]:
- generic [ref=e584]: "failed: 114"
- generic [ref=e586]: "malformed: 52"
- generic [ref=e588]: "ok: 181"
- generic [ref=e590]: "succeeded: 538"
- generic [ref=e592]: "truncated: 9"
- generic [ref=e594]: "unverifiable: 6911"
- generic [ref=e596]:
- heading "◆ Category Breakdown" [level=2] [ref=e597]:
- generic [ref=e598]: ◆
- text: Category Breakdown
- generic [ref=e599]:
- generic [ref=e600]:
- generic [ref=e602]: coding_refactor
- generic [ref=e603]: "41"
- generic [ref=e604]:
- generic [ref=e606]: general_chat
- generic [ref=e607]: "5"
- generic [ref=e608]:
- generic [ref=e610]: tool_use_agentic
- generic [ref=e611]: "2"
- generic [ref=e612]:
- generic [ref=e614]: docs_writing
- generic [ref=e615]: "2"
- generic [ref=e616]:
- heading "⚠ Warnings" [level=2] [ref=e617]:
- generic [ref=e618]: ⚠
- text: Warnings
- generic [ref=e619]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e623]:
- generic [ref=e624]: ◈
- text: History
- generic [ref=e625]:
- button "1h" [ref=e626] [cursor=pointer]
- button "6h" [ref=e627] [cursor=pointer]
- button "24h" [ref=e628] [cursor=pointer]
- button "7d" [ref=e629] [cursor=pointer]
- button "30d" [ref=e630] [cursor=pointer]
- generic [ref=e633]:
- heading "⚙ Controls" [level=2] [ref=e634]:
- generic [ref=e635]: ⚙
- text: Controls
- generic [ref=e636]:
- generic [ref=e637]:
- text: Operational Triggers
- generic [ref=e638]: — fire-and-forget maintenance jobs
- generic [ref=e639]:
- button "↻ Refresh Catalog" [ref=e640] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e641] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e642] [cursor=pointer]
- button "⟳ Restart Service" [ref=e643] [cursor=pointer]
- generic [ref=e646]:
- text: Runtime Knobs
- generic [ref=e647]: — toggle in-memory; no config.yaml write
- generic [ref=e648]:
- generic [ref=e649]:
- text: Persisted Config
- generic [ref=e650]: — allowlisted keys only; persisted to config.yaml
- table [ref=e651]:
- rowgroup [ref=e652]:
- row [ref=e653]:
- cell "Loading config…" [ref=e654]
- button "Save All Config" [ref=e655] [cursor=pointer]

View File

@@ -0,0 +1,812 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 10:48:26 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e656]:
- generic [ref=e657]:
- generic [ref=e658]: 55.0%
- generic [ref=e660]: 55.0%
- generic [ref=e661]: 8279 calls (30d)
- generic [ref=e662]:
- generic [ref=e663]: Plan
- generic [ref=e664]: 6.25 kWh
- generic [ref=e665]: Metered (30d)
- generic [ref=e666]: 3.43556 kWh
- generic [ref=e667]: Calls (30d)
- generic [ref=e668]: "8279"
- generic [ref=e669]: Resets
- generic [ref=e670]: 2026-07-30
- generic [ref=e671]: Note
- generic [ref=e672]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e31]:
- heading "◈ Model Availability" [level=2] [ref=e32]:
- generic [ref=e33]: ◈
- text: Model Availability
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "model" [ref=e38]
- columnheader "provider" [ref=e39]
- columnheader "tier" [ref=e40]
- columnheader "status" [ref=e41]
- columnheader "override" [ref=e42]
- rowgroup [ref=e43]:
- row [ref=e673]:
- cell "deepseek-v4-flash" [ref=e674]
- cell "neuralwatt" [ref=e675]
- cell "2" [ref=e676]
- cell "active" [ref=e677]
- cell "active" [ref=e678]:
- combobox [ref=e679]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e680]:
- cell "deepseek-v4-flash-flex" [ref=e681]
- cell "neuralwatt" [ref=e682]
- cell "2" [ref=e683]
- cell "active" [ref=e684]
- cell "active" [ref=e685]:
- combobox [ref=e686]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e687]:
- cell "gemma-4-31b" [ref=e688]
- cell "neuralwatt" [ref=e689]
- cell "1" [ref=e690]
- cell "active" [ref=e691]
- cell "active" [ref=e692]:
- combobox [ref=e693]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e694]:
- cell "glm-5.2-fast" [ref=e695]
- cell "neuralwatt" [ref=e696]
- cell "2" [ref=e697]
- cell "active" [ref=e698]
- cell "active" [ref=e699]:
- combobox [ref=e700]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e701]:
- cell "glm-5.2-flex" [ref=e702]
- cell "neuralwatt" [ref=e703]
- cell "3" [ref=e704]
- cell "active" [ref=e705]
- cell "active" [ref=e706]:
- combobox [ref=e707]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e708]:
- cell "glm-5.3" [ref=e709]
- cell "neuralwatt" [ref=e710]
- cell "3" [ref=e711]
- cell "active" [ref=e712]
- cell "active" [ref=e713]:
- combobox [ref=e714]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e715]:
- cell "kimi-k2.7-code" [ref=e716]
- cell "neuralwatt" [ref=e717]
- cell "3" [ref=e718]
- cell "active" [ref=e719]
- cell "active" [ref=e720]:
- combobox [ref=e721]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e722]:
- cell "kimi-k2.7-code-fast" [ref=e723]
- cell "neuralwatt" [ref=e724]
- cell "2" [ref=e725]
- cell "active" [ref=e726]
- cell "active" [ref=e727]:
- combobox [ref=e728]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e729]:
- cell "kimi-k2.7-code-flex" [ref=e730]
- cell "neuralwatt" [ref=e731]
- cell "3" [ref=e732]
- cell "active" [ref=e733]
- cell "active" [ref=e734]:
- combobox [ref=e735]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e736]:
- cell "kimi-k3" [ref=e737]
- cell "neuralwatt" [ref=e738]
- cell "3" [ref=e739]
- cell "active" [ref=e740]
- cell "active" [ref=e741]:
- combobox [ref=e742]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e743]:
- cell "kimi-k3-fast" [ref=e744]
- cell "neuralwatt" [ref=e745]
- cell "2" [ref=e746]
- cell "active" [ref=e747]
- cell "active" [ref=e748]:
- combobox [ref=e749]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e750]:
- cell "kimi-k3-flex" [ref=e751]
- cell "neuralwatt" [ref=e752]
- cell "3" [ref=e753]
- cell "active" [ref=e754]
- cell "active" [ref=e755]:
- combobox [ref=e756]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e757]:
- cell "qwen3.6-35b" [ref=e758]
- cell "neuralwatt" [ref=e759]
- cell "3" [ref=e760]
- cell "active" [ref=e761]
- cell "active" [ref=e762]:
- combobox [ref=e763]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- row [ref=e764]:
- cell "qwen3.6-35b-fast" [ref=e765]
- cell "neuralwatt" [ref=e766]
- cell "2" [ref=e767]
- cell "active" [ref=e768]
- cell "active" [ref=e769]:
- combobox [ref=e770]:
- option "active" [selected]
- option "deprecated"
- option "stale"
- generic [ref=e46]:
- generic [ref=e47]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e48]:
- generic [ref=e49]: ⟁
- text: Recent Decisions
- generic [ref=e50]: (50)
- table [ref=e52]:
- rowgroup [ref=e53]:
- row [ref=e54]:
- columnheader "time" [ref=e55]
- columnheader "kind" [ref=e56]
- columnheader "category" [ref=e57]
- columnheader "tier" [ref=e58]
- columnheader "model" [ref=e59]
- columnheader "cost" [ref=e60]
- columnheader "prof" [ref=e61]
- rowgroup [ref=e62]:
- row [ref=e771]:
- cell "03:22:00 AM" [ref=e772]
- cell "chat" [ref=e773]
- cell "general_chat" [ref=e775]
- cell "2" [ref=e776]
- cell "deepseek-v4-flash / neuralwatt" [ref=e777]
- cell "$0.0041" [ref=e778]
- cell "1.00" [ref=e779]
- row [ref=e780]:
- cell "03:23:11 AM" [ref=e781]
- cell "chat" [ref=e782]
- cell "tool_use_agentic" [ref=e784]
- cell "3" [ref=e785]
- cell "kimi-k2.7-code / neuralwatt" [ref=e786]
- cell "$0.0195" [ref=e787]
- cell "1.00" [ref=e788]
- row [ref=e789]:
- cell "03:23:16 AM" [ref=e790]
- cell "chat" [ref=e791]
- cell "general_chat" [ref=e793]
- cell "2" [ref=e794]
- cell "deepseek-v4-flash / neuralwatt" [ref=e795]
- cell "$0.0041" [ref=e796]
- cell "1.00" [ref=e797]
- row [ref=e798]:
- cell "03:24:43 AM" [ref=e799]
- cell "chat" [ref=e800]
- cell "general_chat" [ref=e802]
- cell "2" [ref=e803]
- cell "deepseek-v4-flash / neuralwatt" [ref=e804]
- cell "$0.0041" [ref=e805]
- cell "1.00" [ref=e806]
- row [ref=e807]:
- cell "03:24:54 AM" [ref=e808]
- cell "chat" [ref=e809]
- cell "general_chat" [ref=e811]
- cell "1" [ref=e812]
- cell "gemma-4-31b / neuralwatt" [ref=e813]
- cell "$0.0047" [ref=e814]
- cell "1.00" [ref=e815]
- row [ref=e816]:
- cell "10:40:21 AM" [ref=e817]
- cell "chat" [ref=e818]
- cell "docs_writing" [ref=e820]
- cell "2" [ref=e821]
- cell "kimi-k2.7-code / neuralwatt" [ref=e822]
- cell "$0.0320" [ref=e823]
- cell "0.89" [ref=e824]
- row [ref=e825]:
- cell "10:40:40 AM" [ref=e826]
- cell "chat" [ref=e827]
- cell "docs_writing" [ref=e829]
- cell "2" [ref=e830]
- cell "kimi-k2.7-code / neuralwatt" [ref=e831]
- cell "$0.0326" [ref=e832]
- cell "0.89" [ref=e833]
- row [ref=e834]:
- cell "10:41:02 AM" [ref=e835]
- cell "chat" [ref=e836]
- cell "coding_refactor" [ref=e838]
- cell "2" [ref=e839]
- cell "deepseek-v4-flash / neuralwatt" [ref=e840]
- cell "$0.0017" [ref=e841]
- cell "1.00" [ref=e842]
- row [ref=e843]:
- cell "10:41:08 AM" [ref=e844]
- cell "chat" [ref=e845]
- cell "coding_refactor" [ref=e847]
- cell "2" [ref=e848]
- cell "deepseek-v4-flash / neuralwatt" [ref=e849]
- cell "$0.0018" [ref=e850]
- cell "1.00" [ref=e851]
- row [ref=e852]:
- cell "10:41:11 AM" [ref=e853]
- cell "chat" [ref=e854]
- cell "coding_refactor" [ref=e856]
- cell "2" [ref=e857]
- cell "deepseek-v4-flash / neuralwatt" [ref=e858]
- cell "$0.0019" [ref=e859]
- cell "1.00" [ref=e860]
- row [ref=e861]:
- cell "10:41:15 AM" [ref=e862]
- cell "chat" [ref=e863]
- cell "coding_refactor" [ref=e865]
- cell "2" [ref=e866]
- cell "deepseek-v4-flash / neuralwatt" [ref=e867]
- cell "$0.0019" [ref=e868]
- cell "1.00" [ref=e869]
- row [ref=e870]:
- cell "10:41:20 AM" [ref=e871]
- cell "chat" [ref=e872]
- cell "coding_refactor" [ref=e874]
- cell "2" [ref=e875]
- cell "deepseek-v4-flash / neuralwatt" [ref=e876]
- cell "$0.0019" [ref=e877]
- cell "1.00" [ref=e878]
- row [ref=e879]:
- cell "10:41:24 AM" [ref=e880]
- cell "chat" [ref=e881]
- cell "coding_refactor" [ref=e883]
- cell "2" [ref=e884]
- cell "deepseek-v4-flash / neuralwatt" [ref=e885]
- cell "$0.0019" [ref=e886]
- cell "1.00" [ref=e887]
- row [ref=e888]:
- cell "10:41:28 AM" [ref=e889]
- cell "chat" [ref=e890]
- cell "coding_refactor" [ref=e892]
- cell "2" [ref=e893]
- cell "deepseek-v4-flash / neuralwatt" [ref=e894]
- cell "$0.0019" [ref=e895]
- cell "1.00" [ref=e896]
- row [ref=e897]:
- cell "10:41:33 AM" [ref=e898]
- cell "chat" [ref=e899]
- cell "coding_refactor" [ref=e901]
- cell "2" [ref=e902]
- cell "deepseek-v4-flash / neuralwatt" [ref=e903]
- cell "$0.0020" [ref=e904]
- cell "1.00" [ref=e905]
- row [ref=e906]:
- cell "10:41:37 AM" [ref=e907]
- cell "chat" [ref=e908]
- cell "coding_refactor" [ref=e910]
- cell "2" [ref=e911]
- cell "deepseek-v4-flash / neuralwatt" [ref=e912]
- cell "$0.0020" [ref=e913]
- cell "1.00" [ref=e914]
- row [ref=e915]:
- cell "10:41:43 AM" [ref=e916]
- cell "chat" [ref=e917]
- cell "coding_refactor" [ref=e919]
- cell "2" [ref=e920]
- cell "deepseek-v4-flash / neuralwatt" [ref=e921]
- cell "$0.0020" [ref=e922]
- cell "1.00" [ref=e923]
- row [ref=e924]:
- cell "10:41:47 AM" [ref=e925]
- cell "chat" [ref=e926]
- cell "coding_refactor" [ref=e928]
- cell "2" [ref=e929]
- cell "deepseek-v4-flash / neuralwatt" [ref=e930]
- cell "$0.0020" [ref=e931]
- cell "1.00" [ref=e932]
- row [ref=e933]:
- cell "10:41:51 AM" [ref=e934]
- cell "chat" [ref=e935]
- cell "coding_refactor" [ref=e937]
- cell "2" [ref=e938]
- cell "deepseek-v4-flash / neuralwatt" [ref=e939]
- cell "$0.0020" [ref=e940]
- cell "1.00" [ref=e941]
- row [ref=e942]:
- cell "10:41:55 AM" [ref=e943]
- cell "chat" [ref=e944]
- cell "coding_refactor" [ref=e946]
- cell "2" [ref=e947]
- cell "deepseek-v4-flash / neuralwatt" [ref=e948]
- cell "$0.0021" [ref=e949]
- cell "1.00" [ref=e950]
- row [ref=e951]:
- cell "10:42:05 AM" [ref=e952]
- cell "chat" [ref=e953]
- cell "coding_refactor" [ref=e955]
- cell "2" [ref=e956]
- cell "deepseek-v4-flash / neuralwatt" [ref=e957]
- cell "$0.0021" [ref=e958]
- cell "1.00" [ref=e959]
- row [ref=e960]:
- cell "10:42:11 AM" [ref=e961]
- cell "chat" [ref=e962]
- cell "coding_refactor" [ref=e964]
- cell "2" [ref=e965]
- cell "deepseek-v4-flash / neuralwatt" [ref=e966]
- cell "$0.0021" [ref=e967]
- cell "1.00" [ref=e968]
- row [ref=e969]:
- cell "10:42:16 AM" [ref=e970]
- cell "chat" [ref=e971]
- cell "coding_refactor" [ref=e973]
- cell "2" [ref=e974]
- cell "deepseek-v4-flash / neuralwatt" [ref=e975]
- cell "$0.0023" [ref=e976]
- cell "1.00" [ref=e977]
- row [ref=e978]:
- cell "10:42:21 AM" [ref=e979]
- cell "chat" [ref=e980]
- cell "coding_refactor" [ref=e982]
- cell "2" [ref=e983]
- cell "deepseek-v4-flash / neuralwatt" [ref=e984]
- cell "$0.0023" [ref=e985]
- cell "1.00" [ref=e986]
- row [ref=e987]:
- cell "10:42:25 AM" [ref=e988]
- cell "chat" [ref=e989]
- cell "coding_refactor" [ref=e991]
- cell "2" [ref=e992]
- cell "deepseek-v4-flash / neuralwatt" [ref=e993]
- cell "$0.0023" [ref=e994]
- cell "1.00" [ref=e995]
- row [ref=e996]:
- cell "10:43:00 AM" [ref=e997]
- cell "chat" [ref=e998]
- cell "coding_refactor" [ref=e1000]
- cell "2" [ref=e1001]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1002]
- cell "$0.0023" [ref=e1003]
- cell "1.00" [ref=e1004]
- row [ref=e1005]:
- cell "10:43:04 AM" [ref=e1006]
- cell "chat" [ref=e1007]
- cell "coding_refactor" [ref=e1009]
- cell "2" [ref=e1010]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1011]
- cell "$0.0023" [ref=e1012]
- cell "1.00" [ref=e1013]
- row [ref=e1014]:
- cell "10:43:09 AM" [ref=e1015]
- cell "chat" [ref=e1016]
- cell "coding_refactor" [ref=e1018]
- cell "2" [ref=e1019]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1020]
- cell "$0.0023" [ref=e1021]
- cell "1.00" [ref=e1022]
- row [ref=e1023]:
- cell "10:43:13 AM" [ref=e1024]
- cell "chat" [ref=e1025]
- cell "coding_refactor" [ref=e1027]
- cell "2" [ref=e1028]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1029]
- cell "$0.0023" [ref=e1030]
- cell "1.00" [ref=e1031]
- row [ref=e1032]:
- cell "10:43:19 AM" [ref=e1033]
- cell "chat" [ref=e1034]
- cell "coding_refactor" [ref=e1036]
- cell "2" [ref=e1037]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1038]
- cell "$0.0024" [ref=e1039]
- cell "1.00" [ref=e1040]
- row [ref=e1041]:
- cell "10:43:25 AM" [ref=e1042]
- cell "chat" [ref=e1043]
- cell "coding_refactor" [ref=e1045]
- cell "2" [ref=e1046]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1047]
- cell "$0.0024" [ref=e1048]
- cell "1.00" [ref=e1049]
- row [ref=e1050]:
- cell "10:43:31 AM" [ref=e1051]
- cell "chat" [ref=e1052]
- cell "coding_refactor" [ref=e1054]
- cell "2" [ref=e1055]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1056]
- cell "$0.0024" [ref=e1057]
- cell "1.00" [ref=e1058]
- row [ref=e1059]:
- cell "10:44:06 AM" [ref=e1060]
- cell "chat" [ref=e1061]
- cell "coding_refactor" [ref=e1063]
- cell "2" [ref=e1064]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1065]
- cell "$0.0024" [ref=e1066]
- cell "1.00" [ref=e1067]
- row [ref=e1068]:
- cell "10:44:22 AM" [ref=e1069]
- cell "chat" [ref=e1070]
- cell "coding_refactor" [ref=e1072]
- cell "2" [ref=e1073]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1074]
- cell "$0.0024" [ref=e1075]
- cell "1.00" [ref=e1076]
- row [ref=e1077]:
- cell "10:44:26 AM" [ref=e1078]
- cell "chat" [ref=e1079]
- cell "coding_refactor" [ref=e1081]
- cell "2" [ref=e1082]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1083]
- cell "$0.0024" [ref=e1084]
- cell "1.00" [ref=e1085]
- row [ref=e1086]:
- cell "10:44:30 AM" [ref=e1087]
- cell "chat" [ref=e1088]
- cell "coding_refactor" [ref=e1090]
- cell "2" [ref=e1091]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1092]
- cell "$0.0024" [ref=e1093]
- cell "1.00" [ref=e1094]
- row [ref=e1095]:
- cell "10:44:36 AM" [ref=e1096]
- cell "chat" [ref=e1097]
- cell "coding_refactor" [ref=e1099]
- cell "2" [ref=e1100]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1101]
- cell "$0.0024" [ref=e1102]
- cell "1.00" [ref=e1103]
- row [ref=e1104]:
- cell "10:44:42 AM" [ref=e1105]
- cell "chat" [ref=e1106]
- cell "coding_refactor" [ref=e1108]
- cell "2" [ref=e1109]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1110]
- cell "$0.0024" [ref=e1111]
- cell "1.00" [ref=e1112]
- row [ref=e1113]:
- cell "10:44:50 AM" [ref=e1114]
- cell "chat" [ref=e1115]
- cell "coding_refactor" [ref=e1117]
- cell "2" [ref=e1118]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1119]
- cell "$0.0025" [ref=e1120]
- cell "1.00" [ref=e1121]
- row [ref=e1122]:
- cell "10:46:57 AM" [ref=e1123]
- cell "chat" [ref=e1124]
- cell "coding_refactor" [ref=e1126]
- cell "2" [ref=e1127]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1128]
- cell "$0.0025" [ref=e1129]
- cell "1.00" [ref=e1130]
- row [ref=e1131]:
- cell "10:47:02 AM" [ref=e1132]
- cell "chat" [ref=e1133]
- cell "coding_refactor" [ref=e1135]
- cell "2" [ref=e1136]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1137]
- cell "$0.0025" [ref=e1138]
- cell "1.00" [ref=e1139]
- row [ref=e1140]:
- cell "10:47:09 AM" [ref=e1141]
- cell "chat" [ref=e1142]
- cell "coding_refactor" [ref=e1144]
- cell "2" [ref=e1145]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1146]
- cell "$0.0025" [ref=e1147]
- cell "1.00" [ref=e1148]
- row [ref=e1149]:
- cell "10:47:13 AM" [ref=e1150]
- cell "chat" [ref=e1151]
- cell "coding_refactor" [ref=e1153]
- cell "2" [ref=e1154]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1155]
- cell "$0.0025" [ref=e1156]
- cell "1.00" [ref=e1157]
- row [ref=e1158]:
- cell "10:47:18 AM" [ref=e1159]
- cell "chat" [ref=e1160]
- cell "tool_use_agentic" [ref=e1162]
- cell "2" [ref=e1163]
- cell "qwen3.6-35b / neuralwatt" [ref=e1164]
- cell "$0.0008" [ref=e1165]
- cell "1.00" [ref=e1166]
- row [ref=e1167]:
- cell "10:47:28 AM" [ref=e1168]
- cell "chat" [ref=e1169]
- cell "coding_refactor" [ref=e1171]
- cell "2" [ref=e1172]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1173]
- cell "$0.0025" [ref=e1174]
- cell "1.00" [ref=e1175]
- row [ref=e1176]:
- cell "10:47:33 AM" [ref=e1177]
- cell "chat" [ref=e1178]
- cell "coding_refactor" [ref=e1180]
- cell "2" [ref=e1181]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1182]
- cell "$0.0025" [ref=e1183]
- cell "1.00" [ref=e1184]
- row [ref=e1185]:
- cell "10:47:38 AM" [ref=e1186]
- cell "chat" [ref=e1187]
- cell "coding_refactor" [ref=e1189]
- cell "2" [ref=e1190]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1191]
- cell "$0.0026" [ref=e1192]
- cell "1.00" [ref=e1193]
- row [ref=e1194]:
- cell "10:47:49 AM" [ref=e1195]
- cell "chat" [ref=e1196]
- cell "coding_refactor" [ref=e1198]
- cell "2" [ref=e1199]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1200]
- cell "$0.0026" [ref=e1201]
- cell "1.00" [ref=e1202]
- row [ref=e1203]:
- cell "10:47:54 AM" [ref=e1204]
- cell "chat" [ref=e1205]
- cell "coding_refactor" [ref=e1207]
- cell "2" [ref=e1208]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1209]
- cell "$0.0026" [ref=e1210]
- cell "1.00" [ref=e1211]
- row [ref=e1212]:
- cell "10:47:59 AM" [ref=e1213]
- cell "chat" [ref=e1214]
- cell "coding_refactor" [ref=e1216]
- cell "2" [ref=e1217]
- cell "deepseek-v4-flash / neuralwatt" [ref=e1218]
- cell "$0.0026" [ref=e1219]
- cell "1.00" [ref=e1220]
- generic [ref=e513]:
- heading "▐ Per-Model Usage" [level=2] [ref=e514]:
- generic [ref=e515]: ▐
- text: Per-Model Usage
- generic [ref=e516]:
- generic [ref=e1221]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e1222]
- generic [ref=e1223]: "4359"
- generic [ref=e1225]: $8.4244 · 1.11650kWh
- generic [ref=e1226]:
- generic "qwen3.6-35b / neuralwatt" [ref=e1227]
- generic [ref=e1228]: "1441"
- generic [ref=e1230]: $1.8349 · 0.22940kWh
- generic [ref=e1231]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e1232]
- generic [ref=e1233]: "907"
- generic [ref=e1235]: $5.3526 · 0.66935kWh
- generic [ref=e1236]:
- generic "gemma-4-31b / neuralwatt" [ref=e1237]
- generic [ref=e1238]: "384"
- generic [ref=e1240]: $1.1055 · 0.16247kWh
- generic [ref=e1241]:
- generic "glm-5.2-fast / neuralwatt" [ref=e1242]
- generic [ref=e1243]: "358"
- generic [ref=e1245]: $4.9423 · 0.61844kWh
- generic [ref=e1246]:
- generic "kimi-k3 / neuralwatt" [ref=e1247]
- generic [ref=e1248]: "151"
- generic [ref=e1250]: $3.1220 · 0.39025kWh
- generic [ref=e1251]:
- generic "glm-5.2-flex / neuralwatt" [ref=e1252]
- generic [ref=e1253]: "130"
- generic [ref=e1255]: $0.6407 · 0.12087kWh
- generic [ref=e1256]:
- generic "kimi-k3-fast / neuralwatt" [ref=e1257]
- generic [ref=e1258]: "99"
- generic [ref=e1260]: $0.5215 · 0.06518kWh
- generic [ref=e1261]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e1262]
- generic [ref=e1263]: "88"
- generic [ref=e1265]: $0.0130 · 0.00325kWh
- generic [ref=e1266]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e1267]
- generic [ref=e1268]: "88"
- generic [ref=e1270]: $0.0807 · 0.01008kWh
- generic [ref=e1271]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e1272]
- generic [ref=e1273]: "88"
- generic [ref=e1275]: $0.0946 · 0.01183kWh
- generic [ref=e1276]:
- generic "kimi-k3-flex / neuralwatt" [ref=e1277]
- generic [ref=e1278]: "88"
- generic [ref=e1280]: $0.2680 · 0.03350kWh
- generic [ref=e577]:
- generic [ref=e578]:
- heading "◉ Verdict Mix" [level=2] [ref=e579]:
- generic [ref=e580]: ◉
- text: Verdict Mix
- generic [ref=e583]:
- generic [ref=e1281]: "failed: 114"
- generic [ref=e1283]: "malformed: 52"
- generic [ref=e1285]: "ok: 181"
- generic [ref=e1287]: "succeeded: 538"
- generic [ref=e1289]: "truncated: 9"
- generic [ref=e1291]: "unverifiable: 6912"
- generic [ref=e596]:
- heading "◆ Category Breakdown" [level=2] [ref=e597]:
- generic [ref=e598]: ◆
- text: Category Breakdown
- generic [ref=e599]:
- generic [ref=e1293]:
- generic [ref=e1295]: coding_refactor
- generic [ref=e1296]: "42"
- generic [ref=e1297]:
- generic [ref=e1299]: general_chat
- generic [ref=e1300]: "4"
- generic [ref=e1301]:
- generic [ref=e1303]: tool_use_agentic
- generic [ref=e1304]: "2"
- generic [ref=e1305]:
- generic [ref=e1307]: docs_writing
- generic [ref=e1308]: "2"
- generic [ref=e616]:
- heading "⚠ Warnings" [level=2] [ref=e617]:
- generic [ref=e618]: ⚠
- text: Warnings
- generic [ref=e619]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e623]:
- generic [ref=e624]: ◈
- text: History
- generic [ref=e625]:
- button "1h" [ref=e626] [cursor=pointer]
- button "6h" [ref=e627] [cursor=pointer]
- button "24h" [ref=e628] [cursor=pointer]
- button "7d" [ref=e629] [cursor=pointer]
- button "30d" [ref=e630] [cursor=pointer]
- generic [ref=e633]:
- heading "⚙ Controls" [level=2] [ref=e634]:
- generic [ref=e635]: ⚙
- text: Controls
- generic [ref=e636]:
- generic [ref=e637]:
- text: Operational Triggers
- generic [ref=e638]: — fire-and-forget maintenance jobs
- generic [ref=e639]:
- button "↻ Refresh Catalog" [ref=e640] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e641] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e642] [cursor=pointer]
- button "⟳ Restart Service" [ref=e643] [cursor=pointer]
- generic [ref=e645]:
- generic [ref=e646]:
- text: Runtime Knobs
- generic [ref=e647]: — toggle in-memory; no config.yaml write
- generic [ref=e1310]:
- generic [ref=e1311]:
- generic [ref=e1312]:
- checkbox [checked] [ref=e1313]
- generic [ref=e1314] [cursor=pointer]
- generic [ref=e1315]: log_route_decisions
- generic [ref=e1316]: "true"
- generic [ref=e1317]:
- generic [ref=e1318]:
- checkbox [checked] [ref=e1319]
- generic [ref=e1320] [cursor=pointer]
- generic [ref=e1321]: log_energy_observations
- generic [ref=e1322]: "true"
- generic [ref=e1323]:
- generic [ref=e1324]: circuit_breaker
- generic [ref=e1325]: "false"
- generic [ref=e1326]:
- generic [ref=e1327]:
- checkbox [checked] [ref=e1328]
- generic [ref=e1329] [cursor=pointer]
- generic [ref=e1330]: local_llm_enabled
- generic [ref=e1331]: "true"
- generic [ref=e1332]:
- generic [ref=e1333]:
- checkbox [ref=e1334]
- generic [ref=e1335] [cursor=pointer]
- generic [ref=e1336]: session_cache_enabled
- generic [ref=e1337]: "false"
- generic [ref=e1338]:
- generic [ref=e1339]:
- checkbox [ref=e1340]
- generic [ref=e1341] [cursor=pointer]
- generic [ref=e1342]: pinch_enabled
- generic [ref=e1343]: "false"
- generic [ref=e1344]:
- generic [ref=e1345]:
- checkbox [ref=e1346]
- generic [ref=e1347] [cursor=pointer]
- generic [ref=e1348]: pinch_relevance_enabled
- generic [ref=e1349]: "false"
- generic [ref=e1350]:
- generic [ref=e1351]: default_flex_preference
- textbox [ref=e1352]: auto
- generic [ref=e1353]: auto
- generic [ref=e648]:
- generic [ref=e649]:
- text: Persisted Config
- generic [ref=e650]: — allowlisted keys only; persisted to config.yaml
- table [ref=e651]:
- rowgroup [ref=e1354]:
- row [ref=e1355]:
- cell "logging.level" [ref=e1356]
- cell [ref=e1357]:
- textbox [ref=e1358]: info
- cell "info" [ref=e1359]
- row [ref=e1360]:
- cell "objective.quality_tolerance" [ref=e1361]
- cell [ref=e1362]:
- textbox [ref=e1363]: "0.1"
- cell "0.1" [ref=e1364]
- row [ref=e1365]:
- cell "objective.max_energy_per_request" [ref=e1366]
- cell [ref=e1367]:
- textbox [ref=e1368]: "null"
- cell "null" [ref=e1369]
- row [ref=e1370]:
- cell "objective.plan_kwh_per_period" [ref=e1371]
- cell [ref=e1372]:
- textbox [ref=e1373]: "6.25"
- cell "6.25" [ref=e1374]
- row [ref=e1375]:
- cell "circuit_breaker.enabled" [ref=e1376]
- cell [ref=e1377]:
- checkbox [ref=e1378]
- cell "false" [ref=e1379]
- row [ref=e1380]:
- cell "session_cache.enabled" [ref=e1381]
- cell [ref=e1382]:
- checkbox [ref=e1383]
- cell "false" [ref=e1384]
- row [ref=e1385]:
- cell "verification.local_llm_enabled" [ref=e1386]
- cell [ref=e1387]:
- checkbox [checked] [ref=e1388]
- cell "true" [ref=e1389]
- row [ref=e1390]:
- cell "pinch.enabled" [ref=e1391]
- cell [ref=e1392]:
- checkbox [ref=e1393]
- cell "false" [ref=e1394]
- row [ref=e1395]:
- cell "pinch.relevance.enabled" [ref=e1396]
- cell [ref=e1397]:
- checkbox [ref=e1398]
- cell "false" [ref=e1399]
- row [ref=e1400]:
- cell "routing.default_flex_preference" [ref=e1401]
- cell [ref=e1402]:
- textbox [ref=e1403]: auto
- cell "auto" [ref=e1404]
- button "Save All Config" [ref=e655] [cursor=pointer]

View File

@@ -0,0 +1,581 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=e4]
- generic [ref=e6]: live
- generic [ref=e7]: 8/29/2026, 11:00:11 AM
- generic [ref=e8]:
- generic [ref=e9]:
- generic [ref=e10]:
- heading "⚡ Quota Meter" [level=2] [ref=e11]:
- generic [ref=e12]: ⚡
- text: Quota Meter
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: 55.2%
- generic [ref=e18]: 55.2%
- generic [ref=e19]: 8372 calls (30d)
- generic [ref=e20]:
- generic [ref=e21]: Plan
- generic [ref=e22]: 6.25 kWh
- generic [ref=e23]: Metered (30d)
- generic [ref=e24]: 3.45156 kWh
- generic [ref=e25]: Calls (30d)
- generic [ref=e26]: "8372"
- generic [ref=e27]: Resets
- generic [ref=e28]: 2026-07-30
- generic [ref=e29]: Note
- generic [ref=e30]: router-metered only; traffic bypassing the router is not counted
- generic [ref=e31]:
- heading "◈ Model Availability" [level=2] [ref=e32]:
- generic [ref=e33]: ◈
- text: Model Availability
- table [ref=e35]:
- rowgroup [ref=e36]:
- row [ref=e37]:
- columnheader "model" [ref=e38]
- columnheader "provider" [ref=e39]
- columnheader "tier" [ref=e40]
- columnheader "status" [ref=e41]
- columnheader "override" [ref=e42]
- rowgroup [ref=e43]:
- row [ref=e44]:
- cell "Loading…" [ref=e45]
- generic [ref=e46]:
- generic [ref=e47]:
- heading "⟁ Recent Decisions (50)" [level=2] [ref=e48]:
- generic [ref=e49]: ⟁
- text: Recent Decisions
- generic [ref=e50]: (50)
- table [ref=e52]:
- rowgroup [ref=e53]:
- row [ref=e54]:
- columnheader "time" [ref=e55]
- columnheader "kind" [ref=e56]
- columnheader "category" [ref=e57]
- columnheader "tier" [ref=e58]
- columnheader "model" [ref=e59]
- columnheader "cost" [ref=e60]
- columnheader "prof" [ref=e61]
- rowgroup [ref=e62]:
- row [ref=e63]:
- cell "10:55:00 AM" [ref=e64]
- cell "chat" [ref=e65]
- cell "tool_use_agentic" [ref=e67]
- cell "2" [ref=e68]
- cell "qwen3.6-35b / neuralwatt" [ref=e69]
- cell "$0.0035" [ref=e70]
- cell "1.00" [ref=e71]
- row [ref=e72]:
- cell "10:55:02 AM" [ref=e73]
- cell "chat" [ref=e74]
- cell "coding_general" [ref=e76]
- cell "2" [ref=e77]
- cell "deepseek-v4-flash / neuralwatt" [ref=e78]
- cell "$0.0027" [ref=e79]
- cell "1.00" [ref=e80]
- row [ref=e81]:
- cell "10:55:03 AM" [ref=e82]
- cell "chat" [ref=e83]
- cell "reasoning_math" [ref=e85]
- cell "2" [ref=e86]
- cell "qwen3.6-35b / neuralwatt" [ref=e87]
- cell "$0.0032" [ref=e88]
- cell "1.00" [ref=e89]
- row [ref=e90]:
- cell "10:55:05 AM" [ref=e91]
- cell "chat" [ref=e92]
- cell "tool_use_agentic" [ref=e94]
- cell "2" [ref=e95]
- cell "qwen3.6-35b / neuralwatt" [ref=e96]
- cell "$0.0035" [ref=e97]
- cell "1.00" [ref=e98]
- row [ref=e99]:
- cell "10:55:08 AM" [ref=e100]
- cell "chat" [ref=e101]
- cell "reasoning_math" [ref=e103]
- cell "2" [ref=e104]
- cell "qwen3.6-35b / neuralwatt" [ref=e105]
- cell "$0.0032" [ref=e106]
- cell "1.00" [ref=e107]
- row [ref=e108]:
- cell "10:55:10 AM" [ref=e109]
- cell "chat" [ref=e110]
- cell "tool_use_agentic" [ref=e112]
- cell "2" [ref=e113]
- cell "qwen3.6-35b / neuralwatt" [ref=e114]
- cell "$0.0035" [ref=e115]
- cell "1.00" [ref=e116]
- row [ref=e117]:
- cell "10:55:12 AM" [ref=e118]
- cell "chat" [ref=e119]
- cell "reasoning_math" [ref=e121]
- cell "2" [ref=e122]
- cell "qwen3.6-35b / neuralwatt" [ref=e123]
- cell "$0.0032" [ref=e124]
- cell "1.00" [ref=e125]
- row [ref=e126]:
- cell "10:55:14 AM" [ref=e127]
- cell "chat" [ref=e128]
- cell "tool_use_agentic" [ref=e130]
- cell "2" [ref=e131]
- cell "qwen3.6-35b / neuralwatt" [ref=e132]
- cell "$0.0041" [ref=e133]
- cell "1.00" [ref=e134]
- row [ref=e135]:
- cell "10:55:15 AM" [ref=e136]
- cell "chat" [ref=e137]
- cell "tool_use_agentic" [ref=e139]
- cell "2" [ref=e140]
- cell "qwen3.6-35b / neuralwatt" [ref=e141]
- cell "$0.0036" [ref=e142]
- cell "1.00" [ref=e143]
- row [ref=e144]:
- cell "10:55:17 AM" [ref=e145]
- cell "chat" [ref=e146]
- cell "reasoning_math" [ref=e148]
- cell "2" [ref=e149]
- cell "qwen3.6-35b / neuralwatt" [ref=e150]
- cell "$0.0032" [ref=e151]
- cell "1.00" [ref=e152]
- row [ref=e153]:
- cell "10:55:21 AM" [ref=e154]
- cell "chat" [ref=e155]
- cell "tool_use_agentic" [ref=e157]
- cell "2" [ref=e158]
- cell "qwen3.6-35b / neuralwatt" [ref=e159]
- cell "$0.0037" [ref=e160]
- cell "1.00" [ref=e161]
- row [ref=e162]:
- cell "10:55:22 AM" [ref=e163]
- cell "chat" [ref=e164]
- cell "general_chat" [ref=e166]
- cell "2" [ref=e167]
- cell "deepseek-v4-flash / neuralwatt" [ref=e168]
- cell "$0.0021" [ref=e169]
- cell "1.00" [ref=e170]
- row [ref=e171]:
- cell "10:55:25 AM" [ref=e172]
- cell "chat" [ref=e173]
- cell "tool_use_agentic" [ref=e175]
- cell "2" [ref=e176]
- cell "qwen3.6-35b / neuralwatt" [ref=e177]
- cell "$0.0037" [ref=e178]
- cell "1.00" [ref=e179]
- row [ref=e180]:
- cell "10:55:27 AM" [ref=e181]
- cell "chat" [ref=e182]
- cell "general_chat" [ref=e184]
- cell "1" [ref=e185]
- cell "gemma-4-31b / neuralwatt" [ref=e186]
- cell "$0.0015" [ref=e187]
- cell "1.00" [ref=e188]
- row [ref=e189]:
- cell "10:55:30 AM" [ref=e190]
- cell "chat" [ref=e191]
- cell "tool_use_agentic" [ref=e193]
- cell "2" [ref=e194]
- cell "qwen3.6-35b / neuralwatt" [ref=e195]
- cell "$0.0037" [ref=e196]
- cell "1.00" [ref=e197]
- row [ref=e198]:
- cell "10:55:35 AM" [ref=e199]
- cell "chat" [ref=e200]
- cell "tool_use_agentic" [ref=e202]
- cell "2" [ref=e203]
- cell "qwen3.6-35b / neuralwatt" [ref=e204]
- cell "$0.0037" [ref=e205]
- cell "1.00" [ref=e206]
- row [ref=e207]:
- cell "10:55:40 AM" [ref=e208]
- cell "chat" [ref=e209]
- cell "tool_use_agentic" [ref=e211]
- cell "2" [ref=e212]
- cell "qwen3.6-35b / neuralwatt" [ref=e213]
- cell "$0.0037" [ref=e214]
- cell "1.00" [ref=e215]
- row [ref=e216]:
- cell "10:55:48 AM" [ref=e217]
- cell "chat" [ref=e218]
- cell "tool_use_agentic" [ref=e220]
- cell "2" [ref=e221]
- cell "qwen3.6-35b / neuralwatt" [ref=e222]
- cell "$0.0037" [ref=e223]
- cell "1.00" [ref=e224]
- row [ref=e225]:
- cell "10:55:51 AM" [ref=e226]
- cell "chat" [ref=e227]
- cell "tool_use_agentic" [ref=e229]
- cell "2" [ref=e230]
- cell "qwen3.6-35b / neuralwatt" [ref=e231]
- cell "$0.0037" [ref=e232]
- cell "1.00" [ref=e233]
- row [ref=e234]:
- cell "10:55:53 AM" [ref=e235]
- cell "chat" [ref=e236]
- cell "general_chat" [ref=e238]
- cell "1" [ref=e239]
- cell "gemma-4-31b / neuralwatt" [ref=e240]
- cell "$0.0015" [ref=e241]
- cell "1.00" [ref=e242]
- row [ref=e243]:
- cell "10:55:59 AM" [ref=e244]
- cell "chat" [ref=e245]
- cell "general_chat" [ref=e247]
- cell "1" [ref=e248]
- cell "gemma-4-31b / neuralwatt" [ref=e249]
- cell "$0.0015" [ref=e250]
- cell "1.00" [ref=e251]
- row [ref=e252]:
- cell "10:56:08 AM" [ref=e253]
- cell "chat" [ref=e254]
- cell "general_chat" [ref=e256]
- cell "1" [ref=e257]
- cell "gemma-4-31b / neuralwatt" [ref=e258]
- cell "$0.0016" [ref=e259]
- cell "1.00" [ref=e260]
- row [ref=e261]:
- cell "10:56:11 AM" [ref=e262]
- cell "chat" [ref=e263]
- cell "tool_use_agentic" [ref=e265]
- cell "2" [ref=e266]
- cell "qwen3.6-35b / neuralwatt" [ref=e267]
- cell "$0.0038" [ref=e268]
- cell "1.00" [ref=e269]
- row [ref=e270]:
- cell "10:56:18 AM" [ref=e271]
- cell "chat" [ref=e272]
- cell "tool_use_agentic" [ref=e274]
- cell "2" [ref=e275]
- cell "qwen3.6-35b / neuralwatt" [ref=e276]
- cell "$0.0038" [ref=e277]
- cell "1.00" [ref=e278]
- row [ref=e279]:
- cell "10:56:22 AM" [ref=e280]
- cell "chat" [ref=e281]
- cell "tool_use_agentic" [ref=e283]
- cell "2" [ref=e284]
- cell "qwen3.6-35b / neuralwatt" [ref=e285]
- cell "$0.0038" [ref=e286]
- cell "1.00" [ref=e287]
- row [ref=e288]:
- cell "10:56:27 AM" [ref=e289]
- cell "chat" [ref=e290]
- cell "tool_use_agentic" [ref=e292]
- cell "2" [ref=e293]
- cell "qwen3.6-35b / neuralwatt" [ref=e294]
- cell "$0.0038" [ref=e295]
- cell "1.00" [ref=e296]
- row [ref=e297]:
- cell "10:56:32 AM" [ref=e298]
- cell "chat" [ref=e299]
- cell "tool_use_agentic" [ref=e301]
- cell "2" [ref=e302]
- cell "qwen3.6-35b / neuralwatt" [ref=e303]
- cell "$0.0038" [ref=e304]
- cell "1.00" [ref=e305]
- row [ref=e306]:
- cell "10:56:37 AM" [ref=e307]
- cell "chat" [ref=e308]
- cell "tool_use_agentic" [ref=e310]
- cell "2" [ref=e311]
- cell "qwen3.6-35b / neuralwatt" [ref=e312]
- cell "$0.0010" [ref=e313]
- cell "1.00" [ref=e314]
- row [ref=e315]:
- cell "10:56:39 AM" [ref=e316]
- cell "chat" [ref=e317]
- cell "tool_use_agentic" [ref=e319]
- cell "2" [ref=e320]
- cell "qwen3.6-35b / neuralwatt" [ref=e321]
- cell "$0.0038" [ref=e322]
- cell "1.00" [ref=e323]
- row [ref=e324]:
- cell "10:56:42 AM" [ref=e325]
- cell "chat" [ref=e326]
- cell "tool_use_agentic" [ref=e328]
- cell "2" [ref=e329]
- cell "qwen3.6-35b / neuralwatt" [ref=e330]
- cell "$0.0017" [ref=e331]
- cell "1.00" [ref=e332]
- row [ref=e333]:
- cell "10:56:44 AM" [ref=e334]
- cell "chat" [ref=e335]
- cell "tool_use_agentic" [ref=e337]
- cell "2" [ref=e338]
- cell "qwen3.6-35b / neuralwatt" [ref=e339]
- cell "$0.0038" [ref=e340]
- cell "1.00" [ref=e341]
- row [ref=e342]:
- cell "10:56:53 AM" [ref=e343]
- cell "chat" [ref=e344]
- cell "tool_use_agentic" [ref=e346]
- cell "2" [ref=e347]
- cell "qwen3.6-35b / neuralwatt" [ref=e348]
- cell "$0.0017" [ref=e349]
- cell "1.00" [ref=e350]
- row [ref=e351]:
- cell "10:57:14 AM" [ref=e352]
- cell "chat" [ref=e353]
- cell "tool_use_agentic" [ref=e355]
- cell "2" [ref=e356]
- cell "qwen3.6-35b / neuralwatt" [ref=e357]
- cell "$0.0017" [ref=e358]
- cell "1.00" [ref=e359]
- row [ref=e360]:
- cell "10:57:19 AM" [ref=e361]
- cell "chat" [ref=e362]
- cell "tool_use_agentic" [ref=e364]
- cell "2" [ref=e365]
- cell "qwen3.6-35b / neuralwatt" [ref=e366]
- cell "$0.0017" [ref=e367]
- cell "1.00" [ref=e368]
- row [ref=e369]:
- cell "10:57:23 AM" [ref=e370]
- cell "chat" [ref=e371]
- cell "tool_use_agentic" [ref=e373]
- cell "2" [ref=e374]
- cell "qwen3.6-35b / neuralwatt" [ref=e375]
- cell "$0.0017" [ref=e376]
- cell "1.00" [ref=e377]
- row [ref=e378]:
- cell "10:57:27 AM" [ref=e379]
- cell "chat" [ref=e380]
- cell "tool_use_agentic" [ref=e382]
- cell "2" [ref=e383]
- cell "qwen3.6-35b / neuralwatt" [ref=e384]
- cell "$0.0018" [ref=e385]
- cell "1.00" [ref=e386]
- row [ref=e387]:
- cell "10:57:37 AM" [ref=e388]
- cell "chat" [ref=e389]
- cell "tool_use_agentic" [ref=e391]
- cell "2" [ref=e392]
- cell "qwen3.6-35b / neuralwatt" [ref=e393]
- cell "$0.0018" [ref=e394]
- cell "1.00" [ref=e395]
- row [ref=e396]:
- cell "10:57:50 AM" [ref=e397]
- cell "chat" [ref=e398]
- cell "tool_use_agentic" [ref=e400]
- cell "2" [ref=e401]
- cell "qwen3.6-35b / neuralwatt" [ref=e402]
- cell "$0.0038" [ref=e403]
- cell "1.00" [ref=e404]
- row [ref=e405]:
- cell "10:57:59 AM" [ref=e406]
- cell "chat" [ref=e407]
- cell "tool_use_agentic" [ref=e409]
- cell "2" [ref=e410]
- cell "qwen3.6-35b / neuralwatt" [ref=e411]
- cell "$0.0038" [ref=e412]
- cell "1.00" [ref=e413]
- row [ref=e414]:
- cell "10:58:01 AM" [ref=e415]
- cell "chat" [ref=e416]
- cell "tool_use_agentic" [ref=e418]
- cell "2" [ref=e419]
- cell "qwen3.6-35b / neuralwatt" [ref=e420]
- cell "$0.0018" [ref=e421]
- cell "1.00" [ref=e422]
- row [ref=e423]:
- cell "10:58:07 AM" [ref=e424]
- cell "chat" [ref=e425]
- cell "tool_use_agentic" [ref=e427]
- cell "2" [ref=e428]
- cell "qwen3.6-35b / neuralwatt" [ref=e429]
- cell "$0.0018" [ref=e430]
- cell "1.00" [ref=e431]
- row [ref=e432]:
- cell "10:58:29 AM" [ref=e433]
- cell "chat" [ref=e434]
- cell "tool_use_agentic" [ref=e436]
- cell "2" [ref=e437]
- cell "qwen3.6-35b / neuralwatt" [ref=e438]
- cell "$0.0018" [ref=e439]
- cell "1.00" [ref=e440]
- row [ref=e441]:
- cell "10:58:50 AM" [ref=e442]
- cell "chat" [ref=e443]
- cell "tool_use_agentic" [ref=e445]
- cell "2" [ref=e446]
- cell "qwen3.6-35b / neuralwatt" [ref=e447]
- cell "$0.0018" [ref=e448]
- cell "1.00" [ref=e449]
- row [ref=e450]:
- cell "10:58:54 AM" [ref=e451]
- cell "chat" [ref=e452]
- cell "tool_use_agentic" [ref=e454]
- cell "2" [ref=e455]
- cell "qwen3.6-35b / neuralwatt" [ref=e456]
- cell "$0.0018" [ref=e457]
- cell "1.00" [ref=e458]
- row [ref=e459]:
- cell "10:59:14 AM" [ref=e460]
- cell "chat" [ref=e461]
- cell "tool_use_agentic" [ref=e463]
- cell "2" [ref=e464]
- cell "qwen3.6-35b / neuralwatt" [ref=e465]
- cell "$0.0019" [ref=e466]
- cell "1.00" [ref=e467]
- row [ref=e468]:
- cell "10:59:27 AM" [ref=e469]
- cell "chat" [ref=e470]
- cell "tool_use_agentic" [ref=e472]
- cell "2" [ref=e473]
- cell "qwen3.6-35b / neuralwatt" [ref=e474]
- cell "$0.0019" [ref=e475]
- cell "1.00" [ref=e476]
- row [ref=e477]:
- cell "10:59:31 AM" [ref=e478]
- cell "chat" [ref=e479]
- cell "tool_use_agentic" [ref=e481]
- cell "2" [ref=e482]
- cell "qwen3.6-35b / neuralwatt" [ref=e483]
- cell "$0.0019" [ref=e484]
- cell "1.00" [ref=e485]
- row [ref=e486]:
- cell "11:00:03 AM" [ref=e487]
- cell "chat" [ref=e488]
- cell "tool_use_agentic" [ref=e490]
- cell "2" [ref=e491]
- cell "qwen3.6-35b / neuralwatt" [ref=e492]
- cell "$0.0039" [ref=e493]
- cell "1.00" [ref=e494]
- row [ref=e495]:
- cell "11:00:05 AM" [ref=e496]
- cell "chat" [ref=e497]
- cell "tool_use_agentic" [ref=e499]
- cell "2" [ref=e500]
- cell "qwen3.6-35b / neuralwatt" [ref=e501]
- cell "$0.0019" [ref=e502]
- cell "1.00" [ref=e503]
- row [ref=e504]:
- cell "11:00:08 AM" [ref=e505]
- cell "chat" [ref=e506]
- cell "tool_use_agentic" [ref=e508]
- cell "2" [ref=e509]
- cell "qwen3.6-35b / neuralwatt" [ref=e510]
- cell "$0.0039" [ref=e511]
- cell "1.00" [ref=e512]
- generic [ref=e513]:
- heading "▐ Per-Model Usage" [level=2] [ref=e514]:
- generic [ref=e515]: ▐
- text: Per-Model Usage
- generic [ref=e516]:
- generic [ref=e517]:
- generic "deepseek-v4-flash / neuralwatt" [ref=e518]
- generic [ref=e519]: "4394"
- generic [ref=e521]: $8.4545 · 1.12261kWh
- generic [ref=e522]:
- generic "qwen3.6-35b / neuralwatt" [ref=e523]
- generic [ref=e524]: "1492"
- generic [ref=e526]: $1.8762 · 0.23457kWh
- generic [ref=e527]:
- generic "kimi-k2.7-code / neuralwatt" [ref=e528]
- generic [ref=e529]: "910"
- generic [ref=e531]: $5.3850 · 0.67339kWh
- generic [ref=e532]:
- generic "gemma-4-31b / neuralwatt" [ref=e533]
- generic [ref=e534]: "388"
- generic [ref=e536]: $1.1110 · 0.16317kWh
- generic [ref=e537]:
- generic "glm-5.2-fast / neuralwatt" [ref=e538]
- generic [ref=e539]: "358"
- generic [ref=e541]: $4.9423 · 0.61844kWh
- generic [ref=e542]:
- generic "kimi-k3 / neuralwatt" [ref=e543]
- generic [ref=e544]: "151"
- generic [ref=e546]: $3.1220 · 0.39025kWh
- generic [ref=e547]:
- generic "glm-5.2-flex / neuralwatt" [ref=e548]
- generic [ref=e549]: "130"
- generic [ref=e551]: $0.6407 · 0.12087kWh
- generic [ref=e552]:
- generic "kimi-k3-fast / neuralwatt" [ref=e553]
- generic [ref=e554]: "99"
- generic [ref=e556]: $0.5215 · 0.06518kWh
- generic [ref=e557]:
- generic "deepseek-v4-flash-flex / neuralwatt" [ref=e558]
- generic [ref=e559]: "88"
- generic [ref=e561]: $0.0130 · 0.00325kWh
- generic [ref=e562]:
- generic "kimi-k2.7-code-fast / neuralwatt" [ref=e563]
- generic [ref=e564]: "88"
- generic [ref=e566]: $0.0807 · 0.01008kWh
- generic [ref=e567]:
- generic "kimi-k2.7-code-flex / neuralwatt" [ref=e568]
- generic [ref=e569]: "88"
- generic [ref=e571]: $0.0946 · 0.01183kWh
- generic [ref=e572]:
- generic "kimi-k3-flex / neuralwatt" [ref=e573]
- generic [ref=e574]: "88"
- generic [ref=e576]: $0.2680 · 0.03350kWh
- generic [ref=e577]:
- generic [ref=e578]:
- heading "◉ Verdict Mix" [level=2] [ref=e579]:
- generic [ref=e580]: ◉
- text: Verdict Mix
- generic [ref=e583]:
- generic [ref=e584]: "failed: 114"
- generic [ref=e586]: "malformed: 52"
- generic [ref=e588]: "ok: 185"
- generic [ref=e590]: "succeeded: 541"
- generic [ref=e592]: "truncated: 9"
- generic [ref=e594]: "unverifiable: 7005"
- generic [ref=e596]:
- heading "◆ Category Breakdown" [level=2] [ref=e597]:
- generic [ref=e598]: ◆
- text: Category Breakdown
- generic [ref=e599]:
- generic [ref=e600]:
- generic [ref=e602]: tool_use_agentic
- generic [ref=e603]: "40"
- generic [ref=e604]:
- generic [ref=e606]: general_chat
- generic [ref=e607]: "5"
- generic [ref=e608]:
- generic [ref=e610]: reasoning_math
- generic [ref=e611]: "4"
- generic [ref=e612]:
- generic [ref=e614]: coding_general
- generic [ref=e615]: "1"
- generic [ref=e616]:
- heading "⚠ Warnings" [level=2] [ref=e617]:
- generic [ref=e618]: ⚠
- text: Warnings
- generic [ref=e619]: "1/14 routable models have no proficiency data — task_category cannot influence their ranking. Run: python eval_proficiency.py"
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=e623]:
- generic [ref=e624]: ◈
- text: History
- generic [ref=e625]:
- button "1h" [ref=e626] [cursor=pointer]
- button "6h" [ref=e627] [cursor=pointer]
- button "24h" [ref=e628] [cursor=pointer]
- button "7d" [ref=e629] [cursor=pointer]
- button "30d" [ref=e630] [cursor=pointer]
- generic [ref=e633]:
- heading "⚙ Controls" [level=2] [ref=e634]:
- generic [ref=e635]: ⚙
- text: Controls
- generic [ref=e636]:
- generic [ref=e637]:
- text: Operational Triggers
- generic [ref=e638]: — fire-and-forget maintenance jobs
- generic [ref=e639]:
- button "↻ Refresh Catalog" [ref=e640] [cursor=pointer]
- button "⚡ Seed Energy" [ref=e641] [cursor=pointer]
- button "✓ Apply Feedback" [ref=e642] [cursor=pointer]
- button "⟳ Restart Service" [ref=e643] [cursor=pointer]
- generic [ref=e646]:
- text: Runtime Knobs
- generic [ref=e647]: — toggle in-memory; no config.yaml write
- generic [ref=e648]:
- generic [ref=e649]:
- text: Persisted Config
- generic [ref=e650]: — allowlisted keys only; persisted to config.yaml
- table [ref=e651]:
- rowgroup [ref=e652]:
- row [ref=e653]:
- cell "Loading config…" [ref=e654]
- button "Save All Config" [ref=e655] [cursor=pointer]

View File

@@ -0,0 +1,95 @@
- generic [active] [ref=f1e1]:
- generic [ref=f1e2]:
- generic [ref=f1e3]:
- heading "admin@router ▸ dashboard" [level=1] [ref=f1e4]
- generic [ref=f1e6]: live
- generic [ref=f1e7]: —
- generic [ref=f1e8]:
- generic [ref=f1e9]:
- generic [ref=f1e10]:
- heading "⚡ Quota Meter" [level=2] [ref=f1e11]:
- generic [ref=f1e12]: ⚡
- text: Quota Meter
- generic [ref=f1e13]: Loading…
- generic [ref=f1e15]:
- heading "◈ Model Availability" [level=2] [ref=f1e16]:
- generic [ref=f1e17]: ◈
- text: Model Availability
- table [ref=f1e19]:
- rowgroup [ref=f1e20]:
- row [ref=f1e21]:
- columnheader "model" [ref=f1e22]
- columnheader "provider" [ref=f1e23]
- columnheader "tier" [ref=f1e24]
- columnheader "status" [ref=f1e25]
- columnheader "override" [ref=f1e26]
- rowgroup [ref=f1e27]:
- row [ref=f1e28]:
- cell "Loading…" [ref=f1e29]
- generic [ref=f1e30]:
- generic [ref=f1e31]:
- heading "⟁ Recent Decisions" [level=2] [ref=f1e32]:
- generic [ref=f1e33]: ⟁
- text: Recent Decisions
- table [ref=f1e35]:
- rowgroup [ref=f1e36]:
- row [ref=f1e37]:
- columnheader "time" [ref=f1e38]
- columnheader "kind" [ref=f1e39]
- columnheader "category" [ref=f1e40]
- columnheader "tier" [ref=f1e41]
- columnheader "model" [ref=f1e42]
- columnheader "cost" [ref=f1e43]
- columnheader "prof" [ref=f1e44]
- rowgroup
- generic [ref=f1e45]:
- heading "▐ Per-Model Usage" [level=2] [ref=f1e46]:
- generic [ref=f1e47]: ▐
- text: Per-Model Usage
- generic [ref=f1e48]: Loading…
- generic [ref=f1e50]:
- heading "◉ Verdict Mix" [level=2] [ref=f1e52]:
- generic [ref=f1e53]: ◉
- text: Verdict Mix
- generic [ref=f1e56]:
- heading "◆ Category Breakdown" [level=2] [ref=f1e57]:
- generic [ref=f1e58]: ◆
- text: Category Breakdown
- generic [ref=f1e59]: Loading…
- heading "⚠ Warnings" [level=2] [ref=f1e62]:
- generic [ref=f1e63]: ⚠
- text: Warnings
- heading "◈ History 1h 6h 24h 7d 30d" [level=2] [ref=f1e66]:
- generic [ref=f1e67]: ◈
- text: History
- generic [ref=f1e68]:
- button "1h" [ref=f1e69] [cursor=pointer]
- button "6h" [ref=f1e70] [cursor=pointer]
- button "24h" [ref=f1e71] [cursor=pointer]
- button "7d" [ref=f1e72] [cursor=pointer]
- button "30d" [ref=f1e73] [cursor=pointer]
- generic [ref=f1e76]:
- heading "⚙ Controls" [level=2] [ref=f1e77]:
- generic [ref=f1e78]: ⚙
- text: Controls
- generic [ref=f1e79]:
- generic [ref=f1e80]:
- text: Operational Triggers
- generic [ref=f1e81]: — fire-and-forget maintenance jobs
- generic [ref=f1e82]:
- button "↻ Refresh Catalog" [ref=f1e83] [cursor=pointer]
- button "⚡ Seed Energy" [ref=f1e84] [cursor=pointer]
- button "✓ Apply Feedback" [ref=f1e85] [cursor=pointer]
- button "⟳ Restart Service" [ref=f1e86] [cursor=pointer]
- generic [ref=f1e89]:
- text: Runtime Knobs
- generic [ref=f1e90]: — toggle in-memory; no config.yaml write
- generic [ref=f1e91]:
- generic [ref=f1e92]:
- text: Persisted Config
- generic [ref=f1e93]: — allowlisted keys only; persisted to config.yaml
- table [ref=f1e94]:
- rowgroup [ref=f1e95]:
- row [ref=f1e96]:
- cell "Loading config…" [ref=f1e97]
- button "Save All Config" [ref=f1e98] [cursor=pointer]

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,3 @@
Total messages: 1 (Errors: 1, Warnings: 0)
[ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0

View File

@@ -0,0 +1,3 @@
Total messages: 1 (Errors: 1, Warnings: 0)
[ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0

View File

@@ -0,0 +1,3 @@
Total messages: 1 (Errors: 1, Warnings: 0)
[ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:8091/favicon.ico:0

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -0,0 +1,210 @@
# Router going unreachable under `systemctl` — investigation & hardening plan
Started 2026-08-29, after the dispatcher went unreachable three times in one
session (opencode: `ConnectionError ... Connection refused`, and the TUI
showing the same). `systemctl --user status` reported `active (running)`
every time — the process never exited, it just stopped accepting
connections. Findings ranked by what's confirmed vs. still open.
---
## What's confirmed
- Each time, `journalctl --user -u llm-router.service` shows the uvicorn
process itself logging `INFO: Shutting down` / `INFO: Waiting for
connections to close. (CTRL+C to force quit)` — this is uvicorn's own
signal handler firing, i.e. the process received **SIGTERM (or SIGINT)**.
- In every occurrence there is **no preceding `Stopping Local LLM model
router...` line from `systemd[862]`**. That line is systemd's own,
unconditional announcement that *it* is running a stop job — it appears
before the two known-good causes (`systemctl stop`, `systemctl restart`,
including the ones this session ran deliberately, which do show it). Its
absence means the signal did not arrive through `systemctl`.
- Because systemd never opened a stop job for these, `TimeoutStopUSec`
(10s, systemd's default) never applied, and `Restart=on-failure` never
fired — that only triggers when the **main process exits**, and it never
did. The unit sat in `active (running)` indefinitely, fully invisible to
systemd's own health tracking, while `curl localhost:8080/health` and
`ss -ltnp` (no listener on 8080) showed it was actually dead to clients.
- `GET /admin/api/restart-service` was never hit — its access-log line
(`POST /api/restart-service`) does not appear in the journal around any of
the incidents, and that endpoint calls `systemctl --user restart`
(`admin.py:691`) which — like the manual restarts — *would* produce the
"Stopping" line. Ruled out.
- Not a suspend/resume cycle (`journalctl` around each incident has no
`suspend|resume|sleep|lid` entries) and not `systemd-oomd` (disabled on
this host, and RSS was ~100-140MB — nowhere near OOM territory).
- Once hung, the process has ~10 open sockets it's waiting to drain
(`/proc/<pid>/fd`) and its listening socket is already closed — consistent
with `_decision_event_stream()` (`dispatcher.py:1396-1415`), the
`/events/decisions` SSE generator, which is an unconditional `while True`
loop that only exits when the client disconnects. It sends a heartbeat
every `SSE_HEARTBEAT_SECONDS = 15`, so it never looks "stuck" to uvicorn —
it's actively working, just working forever. Any TUI or admin-dashboard tab
left open holds one of these connections open across a restart attempt.
- **Fix applied for the *symptom*, already shipped** (commit `b9220ed`):
`ExecStart` now passes `--timeout-graceful-shutdown 5`
(`deploy/llm-router.service`, mirrored to
`~/.config/systemd/user/llm-router.service`). This caps uvicorn's own
drain wait at 5s independent of whether systemd is tracking a stop job, so
a bare SIGTERM now converges to a clean exit instead of hanging forever.
Verified live: the process restarted under this flag has picked it up
(confirmed via `ps -o cmd` showing the flag on the running command).
## What's still open: who sends the signal
The symptom (hangs forever) is fixed. The **trigger** (something delivers
SIGTERM outside of `systemctl`) is not identified, and will keep firing every
10-20 minutes based on tonight's timeline (21:36, 21:49, 22:08, 22:23, 01:42,
01:58 — irregular, not matching either systemd timer's schedule:
`llm-router-poller.timer` is 2h, `llm-router-seed.timer` is 6h).
### H1 — Manual `kill`/`pkill`/process manager in another pane
Two of the six restarts tonight (22:23:07, 01:42:16) exactly match
`systemctl --user restart llm-router.service` in this pane's
`~/.zsh_history`. The other four don't appear in this pane's history, but
there are two other active tmux panes (pts/2, pts/3, an opencode session)
whose shell history hasn't necessarily flushed to disk yet. A `pkill -f
uvicorn`, an `htop`/`btop` kill keystroke, or a `kill <pid>` typed while
cleaning up a stale process in one of those panes would produce exactly this
signature: signal delivered, no systemd stop job.
**How to confirm, without more guessing**: `auditctl` is present on this
host but needs `sudo`, which isn't passwordless here. One-time setup (run
this yourself, since it needs your password):
```bash
sudo auditctl -a always,exit -F arch=b64 -S kill -F a1=15 -k routerkill
sudo auditctl -a always,exit -F arch=b64 -S tgkill -F a2=15 -k routerkill
```
Then after the next hang:
```bash
sudo ausearch -k routerkill -i | tail -40
```
This resolves the exact calling PID, command, and parent PID for every
SIGTERM (`a1=15`/`a2=15` is the signal-number filter) sent anywhere on the
system — it will name the culprit on the next occurrence, whether it's a
shell, a TUI keybinding, or something else entirely.
### H2 — In-process signal provenance (fallback if you'd rather not touch audit)
If `auditctl` is unwanted, the alternative is to have `dispatcher.py` itself
report who signaled it. Python's `signal.signal()` handler doesn't expose the
sender, but `signal.sigwaitinfo()` does (`si_pid`, `si_uid`) when the signal
is blocked from the default async handler and waited on synchronously. This
would mean overriding uvicorn's own SIGTERM handling — a real change to
shutdown behavior, not just an observation — so it's a fallback, not the
first move. Concretely: block SIGTERM in the main thread at startup
(`signal.pthread_sigmask`), spawn a daemon thread that blocks on
`signal.sigwaitinfo([signal.SIGTERM])`, logs `si_pid`/`si_uid`, resolves
`/proc/<si_pid>/comm` and `/proc/<si_pid>/cmdline`, and *then* re-raises
SIGTERM to itself so uvicorn's normal shutdown still runs. Only worth
building if H1's `auditctl` approach is blocked (e.g. no sudo access at all).
### H3 — A systemd timer or path unit we haven't found
Checked and ruled out: only `llm-router-poller.timer` (2h) and
`llm-router-seed.timer` (6h) reference this unit family
(`systemctl --user list-timers`), and neither's schedule lines up with any
incident tonight. No path units exist under
`~/.config/systemd/user/*.path`. Not revisiting unless H1 comes back
negative.
### H4 — opencode or its plugins
Checked `deploy/opencode-plugin/router-outcome.js` (the only opencode plugin
touching this router) — it only does `fetch(ROUTER + "/outcome")`, no process
management, no signals. The `oh-my-openagent` lsp-daemon processes
(`304864`, `502927`) are unrelated node processes with no visibility into
this service. No code path here sends a signal. Ruled out unless new
evidence surfaces.
## Recommended next step
Run the two `auditctl` commands under H1 once (needs your sudo password —
type it interactively in one of your panes, not through me). Leave them
running; they're cheap (`kill`/`tgkill` syscalls are rare in the ambient
workload of this box). Next time the router drops, `sudo ausearch -k
routerkill -i` names the sender in one shot, which turns four remaining
hypotheses into zero. Everything else in this doc is instrumentation for if
that comes back empty.
**Status: armed 2026-08-29 ~02:05 EDT**, then immediately caught a live
occurrence at 02:06:49-54. Result was a false lead, but an instructive one:
```
type=SYSCALL msg=audit(08/29/2026 02:06:54.260:103) : arch=x86_64
syscall=tgkill success=yes exit=0 a0=0x81d9a a1=0x81d9a a2=SIGTERM ...
ppid=862 pid=531866 ... comm=uvicorn exe=/usr/bin/python3.14 key=routerkill
```
`a0`/`a1` both decode to `531866` — the process's own PID. This is **not**
an external actor. It's uvicorn's own shutdown machinery
(`.venv/.../uvicorn/server.py:315-338`, `capture_signals()`): its custom
SIGTERM handler only sets flags, so after graceful shutdown completes it
deliberately re-delivers the same signal to itself via
`signal.raise_signal()` so the process actually terminates the normal way.
Confirmed by timing: `"Shutting down"` first logged at `02:06:49`; this
self-raise is at `02:06:54` — exactly `--timeout-graceful-shutdown 5` later.
This self-raise happens at the end of **every** signal-triggered shutdown,
including ordinary `systemctl restart`s, so on its own it proves nothing
about the trigger.
The real question is what delivered the *original* SIGTERM at `02:06:49`.
Searched the full window (`-ts 02:00:40 -te 02:08:00`, both `kill` and
`tgkill`, system-wide) and found **only the one self-raise record** — no
external `kill`/`tgkill` syscall anywhere in that window. So the original
delivery didn't go through either of those two syscalls. Extended the watch
to the other three ways Linux can deliver a signal:
```bash
sudo auditctl -a always,exit -F arch=b64 -S pidfd_send_signal -F a1=15 -k routerkill
sudo auditctl -a always,exit -F arch=b64 -S rt_sigqueueinfo -F a1=15 -k routerkill
sudo auditctl -a always,exit -F arch=b64 -S rt_tgsigqueueinfo -F a2=15 -k routerkill
```
`pidfd_send_signal` is the leading suspect — it's the modern, race-free
signal syscall some tools (and some systemd internals) now prefer over the
classic `kill`/`tgkill`, and it wouldn't have matched either of the first two
rules. Next occurrence: search all five keys, not just two, and specifically
look for a record **before** the self-raise (which will always be ~5s after
the `"Shutting down"` log line and should now be treated as noise, not
signal).
**Status: all 5 rules armed 2026-08-29 ~02:13 EDT** (confirmed via `sudo
auditctl -l`). A silent occurrence at `02:12:33` (self-healed by
`Restart=always` before anyone noticed — `Shutting down` at `02:12:33`, back
up at `02:12:43`) landed 25 seconds *before* the 3 new rules were added
(`CONFIG_CHANGE` records at `02:13:04`), so it's an incomplete but useful
data point: `sudo ausearch -k routerkill -i` for that window shows plenty of
unrelated `kill` traffic (a `timeout 5 curl` test artifact at `02:10:50`; a
Chrome thread pool killing its own subprocesses at `02:12:23`, none of them
targeting the router's PID) and the expected self-raise at `02:12:38`
(target `532910`, confirming which MainPID this crash belonged to) — but
**no `kill`/`tgkill` record anywhere delivering to `532910`/`0x821ae`
itself.** So the original delivery for this occurrence definitely didn't use
either of the first two watched syscalls, which is exactly what the
`pidfd_send_signal` hypothesis predicts. That rule (plus the two
`rt_*sigqueueinfo` ones) simply wasn't armed yet for this one. Waiting on
the next occurrence now that all 5 are live.
When it happens: `sudo ausearch -k routerkill -i` and look for a record
targeting the router's PID that is *not* ~5s after the "Shutting down" log
line (that offset is always the harmless uvicorn self-raise) and *not*
`comm=uvicorn` — that's the real sender.
**Interim mitigation shipped regardless of root cause**: `Restart=on-failure`
→ `Restart=always` in `llm-router.service` (both the live unit and
`deploy/llm-router.service`). Systemd's `on-failure` policy explicitly
excludes clean termination by SIGTERM/SIGINT from auto-restart (its
assumption: SIGTERM means someone deliberately asked it to stop). That
assumption was wrong for these events, and combined with the graceful-
shutdown-timeout fix (the process now exits cleanly *instead of* hanging),
it meant every occurrence left the service `inactive (dead)` until a human
noticed and restarted it by hand. `Restart=always` self-heals within
`RestartSec=5s` for any termination *except* an explicit `systemctl stop`,
which systemd still honors correctly regardless of the Restart= policy.

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

View File

@@ -0,0 +1,116 @@
# Review: `admin-visual-fixes` implementation
**What it was reviewing:** the 8-todo `admin-visual-fixes` plan
(`code_plans/.omo/plans/admin-visual-fixes.md`), after its orchestration
reported 8/8 complete and F1-F4 all APPROVE. Verified against the actual
diff and the plan's own saved QA evidence
(`code_plans/.omo/evidence/task-8-admin-visual-fixes-console.txt`), not the
orchestration summary.
## Verdict: 6 of 8 todos are genuinely fixed; Todo 2 is not, and the plan's own evidence file proves it
### Confirmed correct
- Todo 1 (quota bar): `gauge-canvas` fully removed, `.quota-bar` markup in place.
- Todo 3 (history chart): canvas id fixed to `history-chart` **and**
`historyChart = renderChart(...)` correctly captures the instance
(`admin/frontend/index.html:610`).
- Todo 4: `knobDisplayValue()` (`index.html:687-696`) correctly unwraps
`{enabled: bool}` before display.
- Todo 5: `renderWarnings` (`index.html:538-547`) rebuilds the `#no-warnings`
placeholder inline instead of referencing a detached node — no more stale
reference.
- Todo 6 (XSS): `task_category` and `modelStr` (selected_model/
selected_provider) are now `escapeHtml()`-wrapped in both
`renderDecisions` and `addDecision`. `rejected_reason`/`runner_up_models`
are never rendered anywhere in the file, so there was nothing to escape
there.
- Todo 7 (config race): correct and thorough.
`_persist_config_value` (`admin.py:349-374`) takes a module-level
`threading.Lock` around the whole load→validate→backup→write→replace
sequence (the right primitive — `admin_config_write` is a *sync* handler,
which FastAPI runs via `anyio.to_thread`, so `asyncio.Lock` would not have
serialized anything), validates before backing up, and writes via
`.tmp` + `os.replace` so a reader never observes a truncated file.
`tests/test_admin_config.py::test_config_concurrent_writes_are_atomic_no_zero_byte_backups`
is a real regression test — a live reader thread asserts the file is
never empty while two writer threads race. `saveAllConfig()`
(`index.html:774-804`) is now a sequential `for...of await` loop, not
`Promise.all`. All 57 admin tests pass.
### Not fixed: Todo 2 (verdict doughnut) — and the evidence already proves it
`renderVerdict` (`index.html:508-536`) fixed the canvas id
(`verdict-canvas` → `verdict-chart`) but never assigns the created chart
back to the `verdictChart` variable:
```js
if (verdictChart) { updateChart('verdict', labels, values, 'doughnut', colors); }
else { renderChart('verdict-chart', labels, values, 'doughnut', colors); } // return value dropped
```
`verdictChart` stays `null` forever, so every 30-second poll
(`REFRESH_MS`) takes the `else` branch again and tries to create a second
Chart.js instance on a canvas that already has one attached. This is
already recorded in the plan's own saved evidence,
`task-8-admin-visual-fixes-console.txt`:
```
Uncaught (in promise) Error: Canvas is already in use. Chart with ID '1' must be
destroyed before the canvas with ID 'verdict-chart' can be reused.
at renderChart (http://127.0.0.1:8091/admin/:831:11)
at renderVerdict (http://127.0.0.1:8091/admin/:531:9)
at loadSnapshot (http://127.0.0.1:8091/admin/:332:4)
```
— repeated 6 times in the log. The doughnut renders once on first load,
then throws and stops updating for the rest of the page's life. This
directly contradicts F3's "clean console" sign-off and Todo 2's own
acceptance criteria ("the verdict doughnut renders"); a single screenshot
taken right after page load wouldn't catch it, but the saved console log
already had it and nobody read it before approving.
**Fix**: mirror what `historyChart` already does correctly two functions
over — capture the return value:
```js
else { verdictChart = renderChart('verdict-chart', labels, values, 'doughnut', colors); }
```
Also update the dead-code branch at `index.html:512`
(`renderChart('verdict-chart', null, 'verdict')` — missing arguments,
never actually reassigns anything either) to just `verdictChart = null;`
without the pointless re-render call, since there's no data to draw.
### Secondary, out-of-scope-but-worth-a-look: `objective.max_energy_per_request` 422s on every "Save All"
Same evidence file shows two `422` responses for
`POST /admin/api/config/objective.max_energy_per_request` during the
save-all QA pass. `config.yaml` has this key as `null`; the config table's
text input renders it as an empty string, and `saveAllConfig`'s
number-coercion (`Number('') → 0`, but the `val.trim() !== ''` guard
correctly keeps it as `''` rather than coercing to `0`) ends up POSTing an
empty string against an `Optional[float]` field, which `RouterConfig`
rejects. Not data-destructive (validation fails before the backup/write
step, exactly as designed) and not part of any of the 8 todos' scope, but
worth a follow-up: null-valued numeric config fields can't currently be
round-tripped through "Save All" without a spurious failure toast.
### Process gap, not a code defect
No commits landed for any of the 8 todos despite the plan's explicit "one
commit per todo (8 total)" commit strategy — `git log` still ends at
`b63234a`, and `admin.py` / `admin/frontend/index.html` remain untracked.
Separately, the live `llm-router.service` has not restarted since 02:35:45
(same PID throughout), so the Todo 7 fix — the one that matters for
production — is sitting on disk but not yet protecting the running
service.
## Recommendation
Fix the one-line `verdictChart` assignment (plus the dead branch at
`:512`), re-run the same Playwright QA against a fresh page load *and* at
least one 30s+ idle period to confirm the console actually stays clean
across a poll cycle this time — a single screenshot won't catch this class
of bug again. Then commit per the plan's own commit strategy. Restarting
`llm-router.service` to pick up the Todo 7 fix is a separate, explicit step
to take afterward, not bundled into the same action.

View File

@@ -0,0 +1,132 @@
# Review: embedding-based pinch relevance + upstream failover/circuit breaker (commit `03f62e2`)
**What it was reviewing:** opencode/Atlas's implementation of both
`code_plans/pinch-embedding-relevance.md` and
`code_plans/upstream-failover-and-circuit-breaker.md` in one commit, per the
`.omo/plans/pinch-embedding-relevance.md` work plan approved earlier.
Verified against the actual diff (`git show 03f62e2`) rather than the commit
message, and ran the full suite directly: 673 passed (up from 562).
## Verdict: correct and faithful except one integration gap the delivered tests don't catch
### Pinch embedding relevance — correct
- `order_by_relevance` (`context_prune.py`): ascending cosine similarity,
correct empty/single-candidate handling.
- `trim_candidates`: extracted cleanly from the old inline logic; both
branches (no-user-turn path, `num_protected_turns <= 0` path) reproduced
exactly, confirmed by diffing line-for-line against the pre-existing
`prune_context` body.
- `prune_context(relevance_order=...)`: the `None` branch is a **byte-for-byte
reproduction** of the original uniform-compression code — confirmed by
direct diff, not just the docstring's claim. The relevance-ordered branch
is structurally bounded to never compress more candidates than the
uniform pass: it walks a single permutation of the same candidate list and
stops early, so its absolute worst case degrades to exactly today's
behavior, never worse. A minor imprecision exists in the savings estimate
used to decide when to stop (compares `len(text)` where the final
compression step uses `len(prose)`), but it cannot violate the "never
compress more than today" invariant and isn't worth a fix on its own.
- `_embed_for_relevance` (`dispatcher.py`): fails closed to `None` on every
path checked — `RequestException`, non-200, unparseable JSON, missing or
malformed embedding vectors — each logging `relevance_unavailable` and
never raising. Matches the spec's core safety requirement exactly.
- `_relevance_order_for`: gates on `cfg.pinch.enabled and
cfg.pinch.relevance.enabled` and `min_candidates`, uses `_text_only` (not
`extract_text`) for candidate text and `_last_user_text` for the query —
both match the decisions adopted at approval time.
- `config.yaml`/`config.py`: `PinchRelevanceConfig` ships `enabled: false`,
matching spec verbatim.
### Upstream failover — correct, including the hard architectural part
- Non-streaming loop (`dispatcher.py`, the `while True:` retry loop): on a
`>=400` response, retries the next `alternatives` candidate instead of
raising immediately, and **does not touch `attempts_used`** — confirmed by
reading the actual diff hunk, not inferring it from a comment. The quality
retry budget and the availability failover are correctly kept separate.
- Streaming path: this was the part of the spec hardest to get right, and
it's genuinely done, not superficially matching the words. The old
`requests.post(...)` call that used to live inside `proxy()` was removed
entirely; a new pre-flight loop opens and status-checks each candidate via
`_open_upstream` *before* `StreamingResponse` is ever constructed, closing
discarded failed connections (`attempt.close()`), and `proxy()` now takes
the already-open healthy connection as a parameter instead of opening its
own. A dead replica genuinely never reaches the client as a `200` with a
broken body.
- `routing.py`'s `exclude_models` filter: a clean 4-line addition matching
the existing `exclude_stale`/`exclude_deprecated` shape exactly, default
`frozenset()` so it's a no-op when unpopulated, no new import into
`routing.py` (the exclusion set is computed in `dispatcher._open_circuits`
and passed in, exactly as specified).
- `circuit_breaker.py` itself is correct and matches `session_cache.py`'s
pure/injected-time pattern precisely: `is_down`, `record_failure`
(exponential backoff, capped at `max_cooldown_seconds`), `record_success`,
`clear` are all implemented as specified and unit-tested in isolation
(`tests/test_circuit_breaker.py`).
- `config.yaml` ships `circuit_breaker.enabled: false` with the exact
cooldown values from the spec (30 / 600 / 2.0). The failover retry itself
is correctly **unconditional** — only the circuit-breaker bookkeeping
(`record_failure`) is gated behind `cfg.circuit_breaker.enabled` — matching
the spec's explicit recommendation to ship failover default-on since its
worst case matches today's behavior exactly.
### `record_success` is never called — the one real gap
**File:** `dispatcher.py`. `circuit_breaker.record_success` is defined
correctly (`circuit_breaker.py:57`) and unit-tested in isolation, but a
repo-wide grep for `record_success` turns up exactly two hits: its own
definition and its own isolated unit test. It is called from **nowhere** in
the actual dispatch path — not the non-streaming loop, not the streaming
pre-flight loop, only mentioned in a docstring comment
(`dispatcher.py:1843`, "...becomes the probe that can clear the entry via
`record_success`") that describes behavior the code next to it doesn't
implement.
**Consequence.** `record_failure`'s cooldown math is
`min(prev.cooldown_seconds * backoff_multiplier, max_cooldown)` — it only
ever looks at whatever is currently stored, with no signal that a success
happened in between. Without `record_success` clearing the entry, **any
model with a failure history has its cooldown monotonically ratchet toward
`max_cooldown_seconds` on every subsequent failure**, even failures
separated by long stretches of trouble-free service. That is the opposite
of "passive recovery rebuilds trust," which was the explicit point of the
design (`code_plans/upstream-failover-and-circuit-breaker.md`: "a success
clears the entry entirely... the next failure after a success restarts at
`initial_cooldown_seconds`, not wherever the backoff had climbed to").
**Why it passed a green suite.** `T12`'s own acceptance criteria
(`.omo/plans/pinch-embedding-relevance.md`) explicitly required asserting
`.record_success` was called "with the survivor" — that assertion was never
written. There is no integration test anywhere at the `chat_completions`
level for the actual failover path; only the isolated helpers
(`_open_circuits`, `_embed_for_relevance`, `_open_upstream`) and
`circuit_breaker.py`'s own unit tests are covered
(`tests/test_dispatcher_helpers.py`, `tests/test_circuit_breaker.py`). The
gap is invisible to the delivered tests because nothing exercises the real
wiring end to end.
**Severity.** Dormant today — `circuit_breaker.enabled: false` by default,
so nothing is affected until it's turned on. Once enabled, the "skip a dead
model for a while" behavior still works correctly (`is_down`'s time
comparison doesn't need `record_success` to function), but the
backoff-resets-after-recovery half of the design does not, which would show
up as a real model gradually becoming permanently circuit-shy over the
service's lifetime rather than what was actually specified.
**Fix.** Call `circuit_breaker.record_success(current_model, provider)`
after a successful (`< 400`) response in the non-streaming loop, and after
the streaming pre-flight loop confirms a healthy connection — both gated by
`cfg.circuit_breaker.enabled`, matching how `record_failure` is already
gated. Then add the integration test T12 already called for: a `503` on the
first candidate, `200` on the second, asserting `record_failure` fired for
the first and `record_success` for the second.
## Recommendation
Land everything else as-is — it's a faithful, carefully verified
implementation of both specs, including the one part (streaming failover)
that was genuinely tricky to get right. Fix `record_success` before
flipping `circuit_breaker.enabled: true` in any real deployment; it's a
two-line addition plus the one integration test that would have caught its
absence.

View File

@@ -0,0 +1,125 @@
# Review: re-check of `resolve-review-findings` lift (commits `e24006a`, `32e1c30`)
**What it was reviewing:** opencode/Atlas's "resolve-review-findings" orchestration,
which claimed to resolve three findings from
[`session-cache-and-baseline-comparator-review.md`](session-cache-and-baseline-comparator-review.md),
[`tui-live-routing-panel-sse-fix-review.md`](tui-live-routing-panel-sse-fix-review.md),
and [`flex-preference-knob-review.md`](flex-preference-knob-review.md), with a
Final Wave of F1-F4 all `[APPROVE]`/`[PASS]`. Spot-checked every claim against
the actual diff rather than the delivery summary. Full suite: 634/634 passing.
## Verdict: 3 of 4 claims hold; the SSE lock gap does not
### Baseline report filter fix — correct, matches the review exactly
`baseline_report.py`'s `load_decisions` now filters
`"kind IN ('route', 'chat', 'dispatch')"` and adds
`"selected_model IS NOT NULL"` — exactly the two clauses
`session-cache-and-baseline-comparator-review.md` asked for. Live check:
`/health`-adjacent `route_decisions` table now yields the real dogfooding
volume instead of the 10-row `kind='route'` sliver. No further action.
### Flex-preference sibling re-gate — correct, was already in HEAD as claimed
`routing.py:267`'s `apply_flex_preference` calls `rejection_reason(sibling,
..., latency_tolerance=BATCH, ...)` before accepting a flex swap, matching
`flex-preference-knob-review.md`'s recommendation. Confirmed the 4 sibling
tests (stale/canary x prefer-flex/force-flex) exist and pass. No further
action.
### SSE loop-capture bug (finding #5) — correctly fixed
`events.subscribe_sse` now captures `asyncio.get_running_loop()` at
subscribe time (called from the `async def events_decisions()` generator,
where it's valid) and stores it in `_sse_loops`, so `publish_decision`
bridges via the *subscriber's* loop rather than calling
`asyncio.get_event_loop()` from the publisher's thread. This is the actual
fix the prior review asked for, and it's the right mechanism. The bounded
queue (`asyncio.Queue(maxsize=events.DEFAULT_QUEUE_SIZE)` at
`dispatcher.py:1381`) and drop-oldest policy (`events._sse_push`) are both
present and match the recommendation too. Dead code (`subscribe`/`unsubscribe`/
`_EVICTED`/`import queue`) is gone, the `queue` stdlib shadowing is resolved
(parameters renamed to `sse_queue`/`decision_queue`), and the dropped
docstring line ("no conversation text, prompt, or session_dir") is restored
at `dispatcher.py:1405-1407`. All of this matches the delivery summary.
### Secondary issue #2 from the same review — NOT fixed, and the new code's own comment says it is
**File:** `events.py:54-73`, `publish_decision`.
**Problem.** The prior review's secondary issue #2 was: "`_sse_subscribers`
iterated without the lock that guards its mutation ... A connect/disconnect
racing a publish can raise `RuntimeError: Set changed size during
iteration`." The rewrite moved from a `set` to a `dict` (`_sse_loops`) but
carried the same gap forward rather than closing it:
```python
# Bridge to async SSE subscribers on their own loops. Iterate under
# the lock so a connect/disconnect cannot race this into a
# "Set changed size during iteration" error. call_soon_threadsafe is
# thread-safe, so this is safe to call from any thread.
for sse_queue, loop in list(_sse_loops.items()):
try:
loop.call_soon_threadsafe(_sse_push, sse_queue, decision)
except RuntimeError:
_sse_subscribers.discard(sse_queue)
_sse_loops.pop(sse_queue, None)
```
The comment says "Iterate under the lock." There is no `with
_subscribers_lock:` anywhere in this function. `subscribe_sse` (line 90),
`unsubscribe_sse` (line 103), and `clear` (line 119) all take
`_subscribers_lock` before touching `_sse_subscribers`/`_sse_loops`;
`publish_decision` — called from arbitrary anyio worker threads on every
routing decision — does not, either for the read (`list(_sse_loops.items())`)
or for the mutation in the `except` branch (`.discard()` / `.pop()`).
**Severity, checked empirically rather than assumed.** Reproduced with a
direct stress test: one thread calling `publish_decision` in a tight loop
while the event loop thread repeatedly subscribes/unsubscribes (churning
`_sse_loops`), ~200k iterations. Zero `RuntimeError`s. On standard
GIL-enabled CPython, `list(dict.items())` for a dict keyed by
identity-hashed objects (`asyncio.Queue`) runs as a single C-level
operation that doesn't yield the GIL mid-iteration, so the exact failure
mode the comment describes is very unlikely to fire in practice — this is
not the live, silently-broken-in-production class of bug #5 was. But it is
a real gap: the invariant the lock exists to provide isn't actually
provided by this function, the comment asserts something false about the
code next to it, and this project's Python version matrix (`tests/`
"Verified on Python 3.10 and 3.14") doesn't rule out a free-threaded
(`--disable-gil`) build, where this would be a genuine data race rather
than a theoretical one.
**Why the tests don't catch it:** `tests/test_events.py` has no test that
references `Lock`, `lock`, `_sse_loops`, or `_subscribers_lock` at all —
confirmed by grep. The new regression tests added this round cover the
loop-capture fix and queue bounding/drop-oldest, not lock safety. This is
the same shape as the original miss: a fixture/test scope that doesn't
exercise the concurrent path the comment is making a claim about.
**Fix.** Wrap the iteration and the `except` branch's mutation in
`with _subscribers_lock:`, matching every other accessor of these two
collections:
```python
with _subscribers_lock:
subscribers = list(_sse_loops.items())
for sse_queue, loop in subscribers:
try:
loop.call_soon_threadsafe(_sse_push, sse_queue, decision)
except RuntimeError:
with _subscribers_lock:
_sse_subscribers.discard(sse_queue)
_sse_loops.pop(sse_queue, None)
```
Then add a test that actually exercises concurrent subscribe/unsubscribe
against a publishing thread (the shape used to reproduce this above), so a
regression here isn't silent a third time.
## Recommendation
Everything else in this lift is solid and needs no rework. Close this one
gap in `events.py::publish_decision` — one `with` block, mirroring the
other three accessors in the same file — and add the concurrent-churn test
described above.

View File

@@ -0,0 +1,183 @@
# Review: `router-admin-portal` implementation
**What it was reviewing:** the shipped admin portal — `admin.py`,
`admin_schema.sql`, `admin/frontend/index.html`, the `dispatcher.py` mount
point, and 9 new test files — against the two documents that set its bar:
`code_plans/admin-portal-gap-analysis.md` (15 pre-implementation findings)
and `code_reviews/router-admin-portal-plan-review.md` (the plan review's
4-item "don't approve as-is" list). All four of that list's items are
checked below against the actual code, not the plan's description of it.
## Verdict: the plan review's 4 required items are all genuinely fixed; one new stored-XSS gap in the dashboard itself was missed by both automated tests and the reported manual QA pass
### The 4 required fixes — all confirmed in the code, not just claimed
1. **`admin_model_overrides` reaches `routing.select_candidates`** — yes.
`dispatcher.py:1857-1877` (`_admin_deprecated_models`) reads the table and
is unioned into `exclude_models` at `dispatcher.py:774`
(`exclude_models=_open_circuits(rows, cfg) | admin_deprecated`), which
feeds `select_candidates` at `:783`. This sits inside `route()` (`:736`),
the single chokepoint every entry point calls (`route_endpoint:1637`,
`dispatch_endpoint:2873`, and all three `chat_completions` call sites at
`:2304/:2325/:2345`), so an override reaches every dispatch path, not just
`/route`. `tests/test_admin_routing_override.py` is a real integration
test, not a unit test of the API alone: it seeds two models, confirms the
better one wins, marks it deprecated through the admin endpoint, and
asserts `/route` now picks the other one — exactly the test the plan
review asked for.
2. **Auth/CSRF given an explicit decision, and the restart response-before-death mechanism specified** — half done. The restart mechanism is
fully addressed: `admin_restart_service` (`admin.py:684-693`) schedules
`_restart_service` via `BackgroundTasks` and returns `{"status":
"restarting"}` immediately, so the response flushes before `systemctl`
sends the process a signal — exactly what was asked for. But the
auth/CSRF half is not: README.md's new section says only "It has no auth
layer yet, so like the other endpoints it is reachable only from
127.0.0.1" — the same read-only-surface answer the plan review said was
being carried forward unexamined, restated rather than re-examined. There
is no CSRF token, no `Origin`/`Referer` check, and no code comment
anywhere in `admin.py` weighing that decision for a surface that now
includes `POST /api/restart-service`, `POST /api/config/{key}`, and the
three subprocess triggers. See the new finding below — it turns out this
gap is worse than CSRF alone, because the dashboard has a stored-XSS hole
that defeats even an Origin check.
3. **History is on-demand `GROUP BY`, not a sampled second database** — yes.
`_history_series` (`admin.py:85-112`) is exactly a bucketed `GROUP BY`
over `route_decisions` / `energy_observations`, no background sampling
loop, no second SQLite file, no lifecycle question. Matches the review's
recommendation exactly.
4. **`ruamel.yaml` pinned in `requirements.txt`** — yes, `ruamel.yaml==0.18.10`
with a comment explaining why pyyaml can't do the job
(`requirements.txt` diff). `admin_config_write` (`admin.py:745-778`)
round-trips through `YAML()`, validates the whole resulting config via
`RouterConfig(**store)` before writing, and takes a timestamped backup
first — comment preservation confirmed by `load_config_store` using
`YAML()` with default (round-trip) mode rather than `safe_load`.
Also confirmed from the gap analysis's other items, cheaply: no
`admin.py → dispatcher` import (`admin.py` imports only `metrics` and
`config`, matching its own docstring's contract at the top of the file);
subprocess triggers use `sys.executable` and
`asyncio.create_subprocess_exec` under a wall-clock deadline
(`admin.py:604-656`), not a blocking `subprocess.run` on the event loop; and
`pinch.relevance.enabled` is exposed as its own knob distinct from
`pinch.enabled` (`_BOOL_KNOBS`, `admin.py:271-279`).
### New finding: stored/live XSS via `task_category`, reachable from the cheapest endpoint in the service, lands in the one surface with write privileges
`TaskRequest.task_category` (`dispatcher.py:214`) is an unvalidated
`Optional[str]` — no enum, no length cap, no character filter. Supplying it
together with `task_tier` and `required_context_tokens` skips the classifier
entirely (`dispatcher.py:739-746`) and its raw value becomes
`classification.task_category`, which `persist_route_decision` writes
verbatim into `route_decisions.task_category` and fans out live to every SSE
subscriber via `events.publish_decision` (`dispatcher.py:1041`). `/route`
itself calls this on every request (`dispatcher.py:1644`) — it is
documented elsewhere in this repo as the free, quota-safe probe endpoint,
which makes it the *lowest-friction* way to get a string stored and pushed
live, not an edge case.
The admin dashboard renders that field back into the DOM in two places
without escaping it:
- `renderDecisions` (`admin/frontend/index.html:490`):
`` <td title="${d.task_category || ''}">${d.task_category || '—'}</td> ``
- `addDecision`, the live SSE row-append path (`index.html:403`): identical
unescaped interpolation.
Both are `tbody.innerHTML = ...` / `insertBefore` on a freshly-built
`<tr>`, so this is a direct HTML injection, not merely an attribute-quoting
issue. The same field, from the same data source, *is* escaped one panel
over in `renderCategoryBreakdown` (`index.html:577`,
`escapeHtml(cat)`) — confirming this is a missed spot rather than a
considered choice, since `escapeHtml` already exists in the file
(`index.html:884-886`) and is used correctly in `renderModels`,
`renderConfig`, and `renderWarnings`.
**Concrete reproduction:**
```bash
curl -s localhost:8080/route -H 'content-type: application/json' -d '{
"task": "x", "task_tier": 1, "required_context_tokens": 0,
"task_category": "<img src=x onerror=fetch(\"/admin/api/restart-service\",{method:\"POST\"})>"
}'
```
The next time an operator has `/admin/` open — including passively, since
the live decisions table updates via SSE without a page reload — the
`onerror` fires in the page that already holds every admin capability:
`restart-service`, allowlisted `config.yaml` writes, and model-availability
overrides, all same-origin `fetch()` calls away with no additional
credential needed. This is why the auth/CSRF gap in item 2 above is worse
than it looks in isolation: an `Origin` check on the write endpoints
wouldn't stop this, because the malicious request originates from
JavaScript already running on `localhost:8080` itself.
No test in the 9 new test files touches HTML escaping or the dashboard's JS
at all (`tests/test_admin_frontend.py` only checks the file is served and
contains the Chart.js tag) — consistent with the orchestration report's "F3
Real manual QA: APPROVE" not having exercised this path.
**Fix**: run `task_category` (and `rejected_reason`, `runner_up_models`, and
any other free-text decision field) through `escapeHtml` in `renderDecisions`
and `addDecision`, matching the pattern already used three other places in
the same file. Validating `task_category` server-side against
`classifier.py`'s known category set would also close it and is probably
worth doing anyway — an unvalidated free-form override string being usable
to skip the classifier is a second, smaller thing worth a look, independent
of the rendering fix.
### Second new finding, caught live in production rather than in review: concurrent config writes actually crash, and can transiently truncate `config.yaml`
The gap analysis's F10 ("concurrent config.yaml edits are a race condition")
was accepted as deferred/"Recommended" on the theory that it's a rare,
low-stakes race. It isn't rare: `saveAllConfig()`
(`admin/frontend/index.html:778-803`) fires every allowlisted key as a
**parallel** `Promise.all` of `POST /admin/api/config/{key}` calls, so the
one-click "Save All Config" button — the documented way to use this
feature — guarantees concurrent writes on every use, not just under load.
Caught live on this host at 02:35:34-36 on 2026-08-29: two of the ten
parallel writes came back `500 Internal Server Error`
(`pinch.enabled`, `routing.default_flex_preference`), both
`TypeError: 'NoneType' object is not subscriptable` at `admin.py:332`
(`_dict_set_at`) — meaning `load_config_store` (`admin.py:336-340`) returned
`None` because it read `config.yaml` while a concurrent request's
`config_path.open("w")` (`admin.py:772`) had already truncated the file to
zero bytes and hadn't finished writing yet. Direct evidence this isn't
theoretical: one of the timestamped backups made during that window,
`config.yaml.bak.1787985334`, is itself **0 bytes** — `shutil.copyfile`
(`admin.py:771`) copied `config.yaml` mid-truncation, so the safety backup
this feature exists to provide was itself corrupted for that request.
`config.yaml` itself survived intact this time (the last writer to finish
happened to write a complete, valid file), but nothing in the current code
guarantees that outcome — the write is an in-place `open("w")` truncate,
not a write-to-temp-file-then-rename, so a crash or a slower writer mid-race
could leave `config.yaml` empty on disk with no valid backup to recover
from.
**Fix**: two independent things, either one closes most of the risk:
(1) make the write atomic — write to `config.yaml.tmp` and `os.replace()`
over the real path, so a reader/writer never observes a truncated file; (2)
serialize config writes with a lock (a module-level `threading.Lock` in
`admin.py` is enough for a single-process service) so overlapping requests
queue instead of interleaving. The frontend's `saveAllConfig()` sending
requests sequentially instead of via `Promise.all` would also avoid
triggering this in the one place it's currently guaranteed to happen, but
the server-side fix is the one that actually closes the bug — a future
`curl` script or a second browser tab hitting `/admin/api/config/*`
concurrently would reproduce it regardless of what the shipped frontend
does.
## Recommendation
Ship the fix for the XSS finding before this goes live somewhere reachable
by more than one trusted user — it's a small, mechanical change
(two `escapeHtml` calls) with an exact reproduction above to verify against.
Everything else checked against the plan review's required list is
genuinely done, not just documented as done: the F1 routing-integration test
in particular is exactly the test that review asked for, not a weaker
substitute. The auth/CSRF decision is still owed a real paragraph (even one
sentence: "accepted, personal single-operator tool, revisit if this becomes
multi-user") rather than the pre-existing read-only-surface sentence carried
forward unchanged — worth doing at the same time as the XSS fix since they're
the same conversation.

View File

@@ -0,0 +1,215 @@
# Review: `router-admin-portal` work plan (pre-implementation)
**What it was reviewing:** `code_plans/.omo/plans/router-admin-portal.md` —
a 10-todo plan to add an integrated `/admin` FastAPI web portal (dashboards
+ operator controls: refresh catalog, reseed energy, restart service,
runtime toggles, allowlisted config writes, model-availability overrides).
Reviewed before any implementation exists, against the actual current code
rather than the plan's own descriptions, and cross-checked against
`code_plans/admin-portal-gap-analysis.md` (a 15-finding critique of an
earlier draft, `code_plans/.omo/drafts/router-admin-portal.md`) to see
whether the final plan actually incorporated what that analysis found.
## Verdict: citations are excellent; one likely functional gap and one unexamined security assumption before this should be approved as-is
### Citation accuracy — verified, essentially flawless
Checked every file:line reference that matters for correctness, not a
sample:
- `dispatcher.py:154` (`cfg`), `:278` (`_db`), `:1306`/`:1353`/`:1417`
(`/health`/`/metrics`/`/events/decisions`) — all exact.
- `config.py:185` (`default_flex_preference: FlexPreference =
FlexPreference.auto`) — confirmed real field, and every one of its four
cited use sites (`dispatcher.py:815, 2364, 2446` plus the docstring
mention at `:186`) — exact.
- Todo 5's entire runtime-toggle line list — eleven citations across seven
config knobs (`circuit_breaker.enabled` at 2556/2573/2820/2823,
`log_route_decisions` at 935, `log_energy_observations` at 1261,
`verification.local_llm_enabled` at 2653/2794, `session_cache.enabled` at
2266/2336, `pinch.enabled` at 2247/2475, the `pinch.relevance` gate at
1827) — every single one checked out exact via direct grep.
One trivial slip: Todo 3 cites `dispatcher.py:2126-2125` for `/v1/models` —
a backwards range (start line > end line); the actual decorator is a single
line at `2126`. Cosmetic, not worth blocking on.
### The admin override table may not actually affect routing — verify before approving
Todo 7 creates `admin_model_overrides(model_id, provider, availability,
reason, updated_at)` specifically to survive `poller.py` overwriting
`models.availability` on every catalog refresh. That part is correctly
diagnosed: `poller.py`'s upsert really does set `availability = "active"`
on every routable row it sees, per the gap analysis's F1 (confirmed
independently, not taken on the gap analysis's word).
But Todo 7's own text only says `GET /admin/api/models` **merges** the
override into what it *returns to the dashboard*. Nowhere across Todos
1–10 is there a step that feeds `admin_model_overrides` into
`routing.select_candidates` / `routing.rejection_reason` — the functions
that actually decide what a live request routes to. As specified, a model
marked "deprecated" through the admin UI would show as deprecated on the
dashboard while the router keeps dispatching to it exactly as before,
because the hard filters still only read `models.availability` /
`models.deprecated`, which the override table never touches.
This is the same shape of defect the last review caught in
`circuit_breaker.record_success`: a feature that is structurally complete
— table, API, tests — but never reaches the code path it exists to affect.
It matters more here, because the stated purpose of this whole control
("mark a flaky model out of rotation") is exactly what was done by hand
with a raw `UPDATE models SET deprecated = 1 ...` earlier this session when
`gemma-4-31b` started failing — if the admin UI's version of that same
action doesn't reach `routing.py`, the feature doesn't do the thing it's
being built for. Add an explicit todo (or fold into Todo 7's acceptance
criteria) requiring `route()`'s candidate-filtering to consult the override
table the same way `_open_circuits` already consults `circuit_breaker.is_down`
for the circuit-breaker exclusion set, and add a test that actually routes
a request against an overridden model and asserts it's excluded — not just
that the API reports it correctly.
### Loopback-only/no-auth is inherited from a read-only posture without being re-examined for a read-write one
The plan states "loopback-only and unauthenticated, matching the router's
current security posture" as an already-made decision. The *current*
posture (`/metrics`, `/health`, SSE decisions) is read-only observability,
and loopback-only is a reasonable ACL for that. This plan adds `POST
/admin/api/restart-service`, on-disk config writes, and subprocess
triggers — a materially larger blast radius reachable by the same
unauthenticated loopback bind. Loopback-only does not mean "only the
operator can reach it": with no auth and no CSRF protection specified
anywhere in the plan, any process running as the same user, or any webpage
the operator's browser visits while the router happens to be running, can
POST to `localhost:8080/admin/api/restart-service` via a plain HTML form —
no preflight required to block a same-origin-policy-naive form POST. This
is a known category of local-dev-server attack (CSRF/DNS-rebinding against
`localhost`) that a pure read-only surface never had to consider. Worth a
deliberate decision — even if the answer stays "loopback-only is
acceptable for a personal single-operator tool" — rather than carrying the
old answer forward because it was already the answer for a different kind
of surface.
Separately, Todo 4's restart-service mechanism is stated aspirationally
("is expected to terminate the serving process; return a `restarting`
status before the process exits") without specifying how the HTTP response
gets flushed to the client before `systemctl --user restart` causes SIGTERM
to land on the same process handling that request. If the endpoint `await`s
the systemctl subprocess inline, the response can't be sent before the
process that would send it is torn down. This needs an explicit
fire-and-forget mechanism (e.g., schedule the restart via `BackgroundTasks`
so it runs after the response is already flushed) named in the plan, not
left as an implementation detail to be discovered while writing Todo 4.
### Three "Recommended" gap-analysis findings are silently absent, not declared as deferred
F9 (no audit trail for admin actions), F10 (no lock against concurrent
config.yaml writes), and F11 (no endpoint to clear circuit-breaker/session
cache state) are all "Recommended" rather than "Must-fix: Yes" in the gap
analysis's own severity table, and none appear in the final plan's
Must-have list. That's a defensible call for a personal, single-operator
tool — but none of the three are named in the plan's "Must NOT have"
section either, which is where deliberate exclusions are supposed to live.
As written, a reader of the final plan alone has no way to tell "considered
and deferred" from "the gap analysis was never actually read." Move them
there explicitly, even as a one-line "deferred: F9/F10/F11, personal-use
tool, revisit if this gets multi-operator" note.
### Cross-repo citations also check out — including the parts I could actually verify
`daashbrd` exists locally (`/home/alee/Sources/daashbrd`), so the plan's
references to it aren't unverifiable hand-waving. Checked directly:
`app/history.py` and `app/frontend/index.html` (2303 lines, matching Todo
12's "fine for a single-page <3k line file" claim in the gap analysis)
exist as cited; `app/main.py:154` is exactly the `StaticFiles.mount(...)`
call and `:303` is exactly the `FileResponse(...)` call the gap analysis's
F12 cited; `tests/test_main.py:23` and `:29` are exactly
`test_frontend_dir_is_absolute_and_exists` and
`test_static_mount_points_to_frontend_dir`, matching "mount checks."
`poller.py`'s `main()`/`__main__` at 298/319, `feedback.py` at 143/171,
`tier.py` at 82/92, and `schema.sql`'s `models` table (line 7) and its
three indexes (244-246) all match Todo 4's and Todo 7's citations exactly
as well. At this point essentially every checkable citation in the entire
plan has been verified — this is the most citation-accurate plan reviewed
in this loop so far.
### Todo 2's history store duplicates data the router already durably records
`admin_history.py` is specified as a **separate SQLite file**
(`router_admin_history.db`) fed by a **background task sampling every
60s** — a pattern lifted directly from `daashbrd`, which doesn't have
anything better to query. This router already isn't in that position:
`energy_observations` records `energy_kwh`, `cost_usd`, and
`carbon_g_co2eq` **per request, as it happens**, and `route_decisions`
records one row per routing decision — both timestamped, both already
durable. `metrics.py`'s existing `/metrics` endpoint already computes
rolling aggregates from `energy_observations` on demand, no sampling loop
required.
A bucketed time-series `GET /admin/api/history` can almost certainly be a
`GROUP BY`-bucketed SQL query against the tables that already exist,
computed on request rather than sampled every 60s into a second database.
That would avoid three problems the current spec creates and doesn't
solve: (1) a background-task lifecycle question that Todo 2 doesn't
actually answer — `admin.py` must never import `dispatcher` per Todo 1, so
nothing in the plan specifies *what* schedules this recurring task against
the running event loop or on which object's startup hook; (2) a second
SQLite file whose freshness is bounded by a 60s sampling interval instead
of being exact; (3) genuinely duplicated data. Given this router's real
traffic volume (one personal deployment; `config.yaml`'s own comments
mention total billed traffic to date as $0.07), query performance against
the existing tables is not a concern that justifies pre-aggregation. Worth
reconsidering before Wave 1, since it's foundational to Todo 2's whole
shape.
### `ruamel.yaml` is a new dependency with no todo to pin it
Todo 6 commits to "ruamel.yaml round-trip preservation" for config writes,
but `ruamel.yaml` is not in `requirements.txt` today (checked directly —
zero matches) and no todo in the plan adds it. `CLAUDE.md`'s own README
section is explicit and deliberate about this: dependencies are pinned
specifically so "a service that restarts on boot shouldn't change its
dependency tree underneath itself," and bumps are meant to be deliberate.
Introducing a new library for a real reason (comment-preserving YAML
writes is the right tool for what F2 needed) is a legitimate, deliberate
addition — but it needs its own explicit line in `requirements.txt` with a
pinned version, and a todo (or an amendment to Todo 6) that says so, rather
than being assumed available.
### Minor: "single self-contained HTML file" and a `/admin/static` mount say two different things
Todo 8 describes "a single self-contained HTML file using Chart.js from
CDN" (matching gap-analysis F12's option (a): one `HTMLResponse` string,
no static mount needed). Todo 9 then separately mounts
`admin/frontend/` under `/admin/static`. If the page is genuinely
self-contained with only a CDN script tag, there's nothing left to serve
under a static mount and it can be dropped; if there *are* separate static
assets planned (a favicon, a stylesheet), "self-contained" should say so.
Small inconsistency, easy to resolve either direction — flagging so
whichever way it goes is a decision rather than a leftover from copying
daashbrd's shape wholesale.
## Recommendation
Don't approve as-is. Four things need an answer before implementation
starts, not after — none of them require redoing work already done well:
1. Confirm — or add a todo requiring — that `admin_model_overrides`
actually reaches `routing.select_candidates`, since without that the
model-management feature doesn't do what it's for.
2. Make an explicit, stated decision about auth/CSRF for the new
write-capable endpoints rather than inheriting the read-only surface's
answer unexamined, and specify the restart-service
response-before-death mechanism concretely.
3. Reconsider Todo 2's separate sampled history database against just
querying `energy_observations`/`route_decisions` directly — it's
simpler, exact instead of 60s-stale, and doesn't leave the background-
task lifecycle question unanswered.
4. Add the missing `requirements.txt` pin for `ruamel.yaml` (or a todo that
does), matching this project's own stated policy on deliberate,
pinned dependency changes.
The citation work underneath all of this is excellent — genuinely the most
accurate plan reviewed in this loop so far, including cross-repo references
into `daashbrd` that all checked out — and doesn't need rework. This is a
plan worth sending back for one more pass on the four points above, not a
rewrite.

View File

@@ -16,9 +16,9 @@ WorkingDirectory=%h/llm-router
# Holds NEURALWATT_API_KEY. Create it with:
# echo "NEURALWATT_API_KEY=$NEURALWATT_API_KEY" > .env && chmod 600 .env
EnvironmentFile=%h/llm-router/.env
ExecStart=%h/llm-router/.venv/bin/uvicorn dispatcher:app --host 127.0.0.1 --port 8080
ExecStart=%h/llm-router/.venv/bin/uvicorn dispatcher:app --host 127.0.0.1 --port 8080 --timeout-graceful-shutdown 5
Restart=on-failure
Restart=always
RestartSec=5s
# Loopback only by default — this service holds a billable API key and has no

View File

@@ -0,0 +1,191 @@
# Spec: restructure README.md around a pitch → features → install → per-command usage shape
**Origin.** Modeled on the structure of
[`dsplce-co/supabase-plus`'s README](https://raw.githubusercontent.com/dsplce-co/supabase-plus/refs/heads/master/README.md)
— its section shape and pacing, not its content or voice. `supabase-plus` is
a Rust CLI tool with crates.io badges, install-method sub-sections, and
punchy, jokey per-command usage blurbs; none of that is this project. What's
being borrowed is purely structural: a short pitch up front, a scannable
features list, a Table of Contents for a doc long enough to need one,
installation broken into named sub-methods, and — the part worth the most —
usage organized **per command**, each with its own short "why you'd want
this" motivation before the command itself, rather than one long undifferentiated
block of curl examples.
This project's own voice stays: dry, evidence-first, "measured live on
[date]" citations, numbers with dates on them. Nothing here asks for new
jokes, new claims, or new numbers — only reorganizing what's already true in
the current 1039-line `README.md` into a shape a first-time reader can
actually navigate.
No README changes accompany this document — this is the spec opencode
builds from.
---
## What's being borrowed, what's being skipped, and why
| Reference element | Verdict | Reasoning |
|---|---|---|
| Org attribution banner (top link) | **Skip** | No org; this is a personal project. |
| Badges row (crates.io, license, version) | **Skip, or minimal** | Nothing here is published to a package registry. A license badge only makes sense once a license exists (see Open Decisions below) — don't badge something that isn't true yet. |
| One-line pitch + short elaboration paragraph | **Adopt** | The current README already has this content (see §1 below) — it's just buried under a heading instead of leading the file. |
| Italic disclaimer right after the pitch | **Adopt, re-aimed** | The reference's is a trademark disclaimer. This project's real analog already exists and is more load-bearing: *"Numbers in this README are measurements, not specifications"* (current README, "What This Is"). Same structural slot — a caveat before anyone trusts a number — different content. |
| Demo GIF | **Optional, not this pass** | `tui.py`'s live dashboard is the natural candidate, but capturing one is a manual terminal recording step, not something a text plan can produce. Leave a placeholder comment (`<!-- TODO: tui.py demo gif -->`) rather than skip the idea entirely. |
| `## Features` — punchy bulleted list | **Adopt, own voice** | See §2. The existing "At a Glance" table already IS a features summary, just in dry-table form instead of scannable bullets. |
| `---` / Table of Contents | **Adopt, straightforwardly** | The current README has **no TOC at all** across ~25 major sections and 1039 lines. This is the single highest-value, lowest-risk item in this whole plan — pure navigation, no content decisions required. |
| `## Installation` with named sub-methods | **Adopt, honestly scoped** | The reference has 6 real install methods (nix/cargo/homebrew/deb/apt/aur). This project has exactly one (`venv` + `pip`) plus a systemd deployment path. Structure as two sub-sections — "Local" and "As a systemd service" — not six fake ones. Do not invent install methods that don't exist to mimic the reference's breadth. |
| `## Usage` — one sub-heading per command, motivation → command → bullets | **Adopt — the main point of this plan** | See §3. This is the biggest actual improvement available: the current "Quick Usage" section is 9 curl commands in one code block with almost no narrative, while the *reasons* for each one are already written elsewhere in the doc, disconnected from the command they justify. |
| `## Requirements` | **Adopt** | Already exists as a sentence in "Setup" ("You need: a Neuralwatt API key, Python 3.10+...") — promote it to its own short section, matching the reference's terse bulleted form. |
| `## Repo & Contributions` | **Needs a decision, see below** | The reference is a public GitHub project soliciting PRs. This repo's remote is a private, self-hosted Gitea instance (`git.adlee.work`) — "PRs welcome" isn't true here. |
| `## License` | **Needs a decision, see below** | No `LICENSE` file exists in this repo (checked directly). The reference names MIT/Apache-2.0 because those are real, chosen licenses. Do not write a license section that names something that isn't actually true. |
## Open decisions (yours, not opencode's to invent)
Two sections in the reference structure map to facts that don't exist yet
in this repo. Both should be **explicit decisions**, not filled in by
guessing during implementation:
1. **License.** Pick one (or explicitly decide "unlicensed / private, not
for redistribution") before a `## License` section gets written. Whatever
is decided, add the matching `LICENSE` file at the same time — a README
section naming a license with no `LICENSE` file in the repo would be the
exact "documented but not actually true" failure mode this project's own
`config.yaml` strictness rules exist to prevent elsewhere.
2. **Repo & Contributions.** Given the remote is a private Gitea instance
rather than a public GitHub project, decide whether this section exists
at all, and if so what it actually says — a link to the internal remote
for your own reference, not an open invitation for outside contributions
that can't reasonably arrive here.
If no decision is made, the plan's default is: **omit both sections** rather
than have opencode fabricate a license or a contribution policy that isn't
real.
## Section-by-section mechanism
### §1. Pitch + elaboration (replaces the top of "What This Is")
Move the existing lead paragraph up to immediately follow the `# Local LLM
Model Router` title, ahead of any subheading — this is exactly what the
reference does (pitch line, then one elaborating paragraph, before any `##`).
Source material already exists verbatim in the current README's opening
paragraph and the "Numbers in this README are measurements, not
specifications" caveat — this section is a move-and-reflow, not a rewrite.
### §2. `## Features`
Reference pattern: one bullet per capability, phrased as a real situation
the reader has been in, followed by the command that fixes it. This
project's own voice should replace "clever/jokey" with "concrete/measured" —
the through-line of the whole existing document. Candidate bullets, pulled
directly from existing content rather than invented:
- `POST /route` — "Want to know what a task would cost before you spend
anything on it?" *(current README: "no provider call, no cost")*
- Per-request energy/cost pricing — "List price ranks models backwards for
this workload; billing is per-kWh, and this router prices per request from
what's actually shaped like your traffic." *(current: "Weighted Scoring"
section)*
- Local vision fallback — "Your cheapest coding model doesn't support
images. This one falls back to a local model instead of 422ing."
*(current: "Local Vision Fallback")*
- Live TUI — "`python tui.py`, a live routing feed with no polling delay."
*(current: "Monitoring")*
- Structural + local-LLM verification — "Every response gets checked for
free before routing ever learns from it." *(current: "Verification
Pipeline")*
Five bullets, matching the reference's scope (3 headline + "and others
like"), not an exhaustive re-listing of every feature — the full detail
still lives in its own section further down, same as the reference's
`## Usage` expands on its `## Features` teasers.
### §3. `## Usage` — the main restructuring work
Current "Quick Usage" is one code block, 9 `curl` commands, minimal
narrative. Reference pattern is one `###` sub-heading per command: a short
paragraph on *why* (often phrased as the problem the reader already has),
then the command, then a bullet list of what it actually does.
This project already has the "why" prose for nearly every command — it's
just located in a different section than the command itself. Concrete
mapping (existing source section → new usage sub-heading):
| New `### ` sub-heading | Command | Source prose already written |
|---|---|---|
| Route without spending anything | `POST /route` | API Endpoints table: "no provider call, no cost" |
| Skip the classifier when you already know the shape | `/route` with overrides | "Input to `/route` and `/dispatch` can include... overrides — these skip the classifier" |
| Actually dispatch and log energy | `POST /dispatch` | API Endpoints table |
| Point any OpenAI-compatible client at it | `/v1/chat/completions`, `/v1/models` | "Pointing a Coding Agent at It" |
| Route overnight/batch work through flex rows | `latency_tolerance: batch` | "auto:batch admits flex rows for async work" |
| Ask an image question | image_url request | "Local Vision Fallback" |
| Force JSON output | `response_format` | Weighted Scoring, hard filter #6 |
| Watch it live | `python tui.py`, `/metrics`, `/events/decisions` | "Monitoring" section, mostly verbatim |
| Probe routing without spending | `router_cli.py` | "Monitoring" section |
This is reorganization, not new writing: every cell in the right column
already exists in the current README. The work is moving each explanation
next to the command it explains, and adding a one-line "why" lead-in where
the existing text is purely descriptive rather than motivating.
### §4. `## Installation`
Two named sub-sections, matching what's actually true:
- **Local (`venv`)** — the existing "Setup" code block, unchanged.
- **As a systemd service** — the existing "Scheduled Jobs (systemd)" table
and its install pointer to `deploy/README.md`.
Do not add a third method. The reference's breadth (6 install paths) exists
because that project targets a general audience installing a CLI tool from
multiple ecosystems; this one has an owner, a GPU, and a fixed deployment
shape.
### §5. `## Requirements`
Pull the existing "Setup" opening sentence — "a Neuralwatt API key, Python
3.10+ (suite verified on 3.10 and 3.14), and an Ollama reachable from
wherever this runs with a classifier model pulled" — into its own short
bulleted section, ahead of Installation, matching the reference's
`requirements`-before-`installation` ordering.
### §6. Table of Contents
Auto-derivable from the final heading structure once the above moves are
made. Every existing `##`/`###` heading gets an entry; nest sub-headings
(Usage's per-command sections, Installation's two methods) the way the
reference nests its own.
## What must NOT change
This is a restructuring of entry points and navigation, not a rewrite of
substance. Everything below stays exactly as it is, moved but not
rewritten:
- The architecture diagram, the module table, the full schema documentation
(`models`/`proficiency`/`energy_observations`/`verifications`/
`route_decisions`), the weighted-scoring mechanism, the classifier
reliability notes, and Known Limitations & Open Items — none of this has
a reference-README equivalent because `supabase-plus` doesn't carry
this much operational depth. It stays as reference material past the new
Usage/Installation front matter, in its current form.
- No new measurements, dates, or claims. Every fact placed into a new
section must already exist verbatim (or near-verbatim, for reflow)
somewhere in the current `README.md`.
- No jokes or voice imported from the reference. "Had buckets locally once,
never found them in prod" works for a Supabase CLI's audience; this
project's own established register — measured numbers, dated
observations, named caveats — is what every other document in this repo
already uses, and the README should not be the one place that departs
from it.
## Recommendation
Build it. The highest-value pieces (a Table of Contents for a 1039-line
document that currently has none, and moving existing "why" prose next to
the commands it explains) require no new facts and no voice risk — they're
pure reorganization. The two sections that need a real answer (License,
Repo & Contributions) should get one from you before opencode touches them;
absent that, the plan's default is to leave them out rather than invent
them.

View File

@@ -0,0 +1,102 @@
# Review: README restructure (commit `14a7653`)
**What it was reviewing:** opencode's implementation of
`documentation_plans/readme-restructure.md` — reshaping `README.md` around a
pitch → features → install → per-command usage structure borrowed from
`supabase-plus`'s README shape. Checked the actual diff (`git show
14a7653`) line by line against the spec's explicit constraints, not just the
new document's surface quality.
## Verdict: the restructuring itself is well done; one section's content was silently dropped, and one TOC link is dead
### What's correct, including the part most likely to go wrong
The spec's two "needs a decision, don't invent" items — a `## License`
section and a `## Repo & Contributions` section — are both correctly
**absent** from the new README. Neither a LICENSE file nor an honest
contributions policy exists yet, and the plan was explicit that opencode
should not fabricate either. Checked directly (`grep -i
"license\|contribut"` against every heading): nothing was invented. This
was the single most important constraint in the spec and it held.
Everything else structural matches: `## Features` uses the five bullets the
plan proposed, pulled from existing claims rather than invented ones;
`## Requirements` is pulled out as its own section; `## Installation` has
exactly the two real sub-methods (`Local (venv)`, `As a systemd service`)
rather than fabricated ones; `## Usage` is restructured into nine
per-command sections matching the plan's mapping table, and spot-checking
"Watch it live" and "Probe routing without spending" confirms the
Monitoring section's content (all five tools: `/metrics`, `/events/decisions`,
`tui.py`, `baseline_report.py`, `router_cli.py`) survived the reflow intact,
just condensed. A Table of Contents now exists where none did before.
### `## Setup`'s "Where Ollama lives" subsection was deleted, not moved
**Confirmed by diff, not inference.** `git show 14a7653 -- README.md`
shows this entire block removed with no corresponding addition anywhere in
the new document:
```
-### Where Ollama lives
-
-```bash
-ollama pull mistral-nemo:12b # or whatever you set as classifier.model
-```
-
-...To use one across a VPN, point **both** endpoints at it:
-...and apply `deploy/ollama-over-vpn.conf` on the serving host — Ollama binds
-`127.0.0.1` by default and will otherwise refuse. Bind it to the VPN address
-rather than `0.0.0.0`: Ollama has no authentication, so anything reaching the
-port can run inference and enumerate your models.
-
-Both endpoints move together because the verifier speaks Ollama's *native*
-API and cannot follow the classifier to a cloud provider...
```
Grepped the current `README.md` for `ollama-over-vpn`, `ollama pull
mistral-nemo`, and `Both endpoints move together` — zero hits. This isn't a
paraphrase living somewhere else; the content is gone.
**Why this matters more than an ordinary trim.** This section carried a real
security instruction, not just background — Ollama has no authentication of
its own, and the doc's own words were the thing telling a reader to bind it
to a VPN address rather than `0.0.0.0`. The plan's own "What must NOT
change" section was explicit: *"Everything below stays exactly as it is,
moved but not rewritten"* and *"No new measurements, dates, or claims"* —
the inverse, dropping an existing one, was never authorized either. The most
likely mechanical cause: the old "Setup" section's opening sentence moved to
`## Requirements` and its venv steps moved to `## Installation`, and
"Where Ollama lives" — a `###` subsection of the same old `## Setup` — seems
to have been left behind in that split rather than carried to either
destination.
### Dead TOC link: "Scheduled Jobs (systemd)"
The Table of Contents (line 74) still has an entry
`- [Scheduled Jobs (systemd)](#scheduled-jobs-systemd)`. That heading no
longer exists — the content it pointed to was correctly moved into `###
As a systemd service` under Installation, which already has its own
correct TOC entry at line 43. The old line was never removed, so it's a
link to nowhere sitting in a document whose main improvement this pass was
*adding working navigation*. One line to delete.
A handful of other headings with em-dashes, backticks, or `&` (e.g.
`` `models` — one row per served model variant``, `Known Limitations & Open
Items`) produced anchor mismatches against a straightforward slugify check,
but GitHub- and Gitea-flavored anchor generation both have their own
non-obvious rules for those characters that a quick script can't be trusted
to reproduce exactly — these are worth a manual click-through in whatever
renderer this repo actually displays in (Gitea, at `git.adlee.work`), not
something to fix on the strength of this review alone.
## Recommendation
Restore "Where Ollama lives" verbatim — it's sitting in `git show
14a7653^:README.md` (the pre-restructure version) if a clean copy is
needed — into `## Installation`, most naturally as a third subsection
alongside `Local (venv)` and `As a systemd service` (it's setup guidance
that applies to either), or folded into `Local (venv)` if a separate
heading feels like too much for one paragraph plus a warning. Delete the
dead `Scheduled Jobs (systemd)` TOC line. Then do one manual pass clicking
every TOC link in the actual Gitea-rendered view, since that's the renderer
that matters here and not something worth guessing at from a script.

187
tests/test_admin_config.py Normal file
View File

@@ -0,0 +1,187 @@
"""Tests for the /admin/api persisted-config endpoints.
``admin.py`` exposes ``GET /admin/api/config`` (the allowlisted config.yaml
values) and ``POST /admin/api/config/{key}`` (persist one allowlisted value to
config.yaml with comment-preserving ruamel.yaml round-trip, a backup copy, and
whole-config validation via ``RouterConfig`` BEFORE anything touches disk).
Each test builds its own isolated router against a temp copy of config.yaml by
passing ``base_dir`` (a temp dir) to ``build_router`` — the real repo
``config.yaml`` is never written. It mounts the router on a fresh FastAPI
TestClient at ``prefix="/admin"``.
"""
from __future__ import annotations
import re
import shutil
import sqlite3
import threading
from pathlib import Path
import pytest
from fastapi import FastAPI
from starlette.testclient import TestClient
from admin import _CONFIG_ALLOWLIST, _persist_config_value, build_router
ROOT = Path(__file__).resolve().parent.parent
_SENTINEL = "# SENTINEL_PRESERVED_12345"
def _insert_sentinel(config_yaml: Path) -> None:
"""Prepend a unique marker comment just above the ``logging:`` map."""
text = config_yaml.read_text()
assert "logging:" in text
config_yaml.write_text(text.replace("logging:", f"{_SENTINEL}\nlogging:", 1))
def _make_db(tmp_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(tmp_path / "admin.db"))
conn.row_factory = sqlite3.Row
return conn
@pytest.fixture
def client(tmp_path, monkeypatch):
"""A TestClient for an isolated admin router over a temp config.yaml copy.
``base_dir`` is tmp_path, so the ``/admin/api/config`` endpoints read and
write ``tmp_path/config.yaml`` — never the repo's copy.
"""
config_yaml = tmp_path / "config.yaml"
shutil.copyfile(ROOT / "config.yaml", config_yaml)
def _db_factory() -> sqlite3.Connection:
return _make_db(tmp_path)
router = build_router(None, _db_factory, base_dir=str(tmp_path))
app = FastAPI()
app.include_router(router, prefix="/admin")
return TestClient(app), config_yaml
def test_config_GET_returns_allowlisted_values(client):
"""GET /admin/api/config returns every allowlisted dotted key."""
tc, _ = client
resp = tc.get("/admin/api/config")
assert resp.status_code == 200
body = resp.json()
for key in (
"logging.level",
"objective.quality_tolerance",
"objective.max_energy_per_request",
"objective.plan_kwh_per_period",
"circuit_breaker.enabled",
"session_cache.enabled",
"verification.local_llm_enabled",
"pinch.enabled",
"pinch.relevance.enabled",
"routing.default_flex_preference",
):
assert key in body
assert body["logging.level"] == "info"
assert body["objective.quality_tolerance"] == 0.10
assert body["routing.default_flex_preference"] == "auto"
def test_config_POST_preserves_comments_and_changes_value(client):
"""A valid write keeps the file's comments AND updates the value."""
tc, config_yaml = client
_insert_sentinel(config_yaml)
resp = tc.post("/admin/api/config/logging.level", json={"value": "warning"})
assert resp.status_code == 200
body = resp.json()
assert body["key"] == "logging.level"
assert body["value"] == "warning"
assert "restart is required" in body["message"]
text = config_yaml.read_text()
assert _SENTINEL in text
assert re.search(r"^\s*level:\s*warning\s*$", text, re.MULTILINE) is not None
def test_config_POST_rejects_non_allowlisted_key(client):
"""classifier.base_url (and any off-allowlist key) is refused with 403."""
tc, _ = client
resp = tc.post("/admin/api/config/classifier.base_url", json={"value": "http://x"})
assert resp.status_code == 403
def test_config_POST_invalid_value_leaves_file_unchanged(client):
"""quality_tolerance=1.5 (>1) fails RouterConfig validation -> 422, no write."""
tc, config_yaml = client
before = config_yaml.read_text()
resp = tc.post(
"/admin/api/config/objective.quality_tolerance", json={"value": 1.5}
)
assert resp.status_code == 422
after = config_yaml.read_text()
assert after == before
def test_config_POST_creates_backup_before_write(client, tmp_path):
"""A successful write produces a ``config.yaml.bak.<ts>`` backup copy."""
tc, config_yaml = client
_insert_sentinel(config_yaml)
resp = tc.post("/admin/api/config/circuit_breaker.enabled", json={"value": True})
assert resp.status_code == 200
backups = sorted(tmp_path.glob("config.yaml.bak.*"))
assert len(backups) == 1
# The backup captured the sentinel comment and the PRE-write value.
backup_text = backups[0].read_text()
assert _SENTINEL in backup_text
assert re.search(r"^\s*enabled:\s*false\s*$", backup_text, re.MULTILINE) is not None
def test_config_concurrent_writes_are_atomic_no_zero_byte_backups(tmp_path):
"""Concurrent writes never truncate config.yaml or leave 0-byte backups."""
config_yaml = tmp_path / "config.yaml"
shutil.copyfile(ROOT / "config.yaml", config_yaml)
path = _CONFIG_ALLOWLIST["logging.level"]
stop_reader = threading.Event()
def reader() -> None:
while not stop_reader.is_set():
try:
text = config_yaml.read_text()
except FileNotFoundError:
continue
assert text.strip() != "", "config.yaml observed empty"
assert "logging:" in text
reader_thread = threading.Thread(target=reader)
reader_thread.start()
def writer(level: str) -> None:
_persist_config_value(config_yaml, path, level)
threads = [
threading.Thread(target=writer, args=("warning",)),
threading.Thread(target=writer, args=("error",)),
]
for t in threads:
t.start()
for t in threads:
t.join()
stop_reader.set()
reader_thread.join()
final = config_yaml.read_text()
assert re.search(
r"^\s*level:\s*(warning|error)\s*$", final, re.MULTILINE
) is not None
backups = list(tmp_path.glob("config.yaml.bak.*"))
assert backups, "expected at least one backup"
for b in backups:
assert b.stat().st_size > 0, f"zero-byte backup: {b}"
assert b.read_text().strip() != ""

View File

@@ -0,0 +1,97 @@
"""Tests for the admin frontend serving route GET /admin/.
The frontend is served as a single FileResponse (no StaticFiles mount). Its
path is derived from ``Path(__file__).resolve().parent`` inside ``admin.py`` —
not from ``base_dir`` — so it resolves to the real repo file even when the
router is built without a base_dir, mirroring the daashbrd H3 portability fix.
"""
from __future__ import annotations
import sqlite3
import subprocess
import sys
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import admin
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
CFG = load_config(str(ROOT / "config.yaml"))
@pytest.fixture
def admin_client(tmp_path, monkeypatch):
"""The real dispatcher app, which mounts admin at the /admin prefix."""
db_path = str(tmp_path / "admin.db")
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
conn.executescript((ROOT / "schema.sql").read_text())
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", db_path)
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_admin_index_returns_html_with_chartjs(admin_client):
"""GET /admin/ returns 200, text/html, and contains the Chart.js tag."""
resp = admin_client.get("/admin/")
assert resp.status_code == 200
assert resp.headers["content-type"].startswith("text/html")
assert "chart.js" in resp.text
def test_admin_index_file_exists_at_module_derived_path():
"""The served file lives at Path(__file__).parent/admin/frontend/index.html."""
expected = (
Path(admin.__file__).resolve().parent / "admin" / "frontend" / "index.html"
)
assert expected.is_file(), f"frontend file missing at {expected}"
def test_admin_frontend_path_does_not_depend_on_base_dir():
"""build_router WITHOUT base_dir already serves '/' on the raw router.
The route must derive its file path from admin.py's own location, not from
the optional base_dir argument, so it stays portable and testable.
"""
probe = (
"import sqlite3\n"
"import admin\n"
"from fastapi import FastAPI\n"
"cfg = admin.load_config('config.yaml')\n"
"def db():\n"
" c = sqlite3.connect(':memory:')\n"
" c.row_factory = sqlite3.Row\n"
" return c\n"
"app = FastAPI()\n"
"app.include_router(admin.build_router(cfg, db))\n"
"from starlette.testclient import TestClient\n"
"with TestClient(app) as client:\n"
" resp = client.get('/')\n"
" assert resp.status_code == 200, resp.status_code\n"
" assert resp.headers['content-type'].startswith('text/html')\n"
)
subprocess.run(
[sys.executable, "-c", probe],
check=True,
cwd=str(ROOT),
capture_output=True,
)
def test_admin_unknown_api_returns_json_404_not_html(admin_client):
"""GET /admin/api/nonexistent is a 404 JSON body, not the HTML page."""
resp = admin_client.get("/admin/api/nonexistent")
assert resp.status_code == 404
assert resp.headers["content-type"].startswith("application/json")
assert "chart.js" not in resp.text

208
tests/test_admin_health.py Normal file
View File

@@ -0,0 +1,208 @@
"""Tests for the /admin/api health and snapshot endpoints.
``admin.py`` is a standalone FastAPI router that mirrors ``metrics.py``'s
contract — never import ``dispatcher``, take ``(conn, cfg)`` explicitly — and
is mounted onto the dispatcher app under the ``/admin`` prefix. These tests
drive a real TestClient GET against the seeded temp DB (never a mock-call
assertion), mirroring ``test_metrics_endpoint.py``.
"""
from __future__ import annotations
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(str(ROOT / "config.yaml"))
def _now() -> datetime:
return datetime.now(timezone.utc)
def _make_db(tmp_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(tmp_path / "test.db"))
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA_SQL)
return conn
def _seed_models(conn: sqlite3.Connection) -> None:
for model_id, tier, context, cost, vision in (
("cheap", 2, 262128, 0.30, 1),
("dear", 2, 262128, 9.00, 0),
("tiny", 1, 131072, 0.10, 1),
):
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, tier, context_window,
effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_vision, supports_json_mode,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, ?, ?, 192500, 16384, ?, ?,
?, 1, 'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(model_id, model_id, tier, context, cost, cost / 3, vision),
)
conn.commit()
def _seed_decision(conn: sqlite3.Connection) -> None:
conn.execute(
"""
INSERT INTO route_decisions (
observed_at, kind, task_category, task_tier, required_context_tokens,
confidence, classifier_ms, classification_source, latency_tolerance,
candidates_considered, selected_model, selected_provider,
runner_up_models, est_cost_usd, est_proficiency,
session_key, tools, images, json_mode, streamed,
flex_preference, flex_swapped, flex_forced
) VALUES (?, 'route', 'coding_general', 2, 100, 0.95, 200,
'classifier', 'interactive', 5, 'cheap', 'neuralwatt',
'[{"model_id":"dear","provider":"neuralwatt"}]',
0.001, 0.9, 'abc123', 0, 0, 0, 0,
'auto', 0, 1)
""",
(_now().isoformat(),),
)
conn.commit()
def _seed_energy(conn: sqlite3.Connection) -> None:
now = _now()
conn.execute(
"INSERT INTO energy_observations "
"(model_id, provider, task_category, completion_tokens, energy_kwh, "
"cost_usd, carbon_g_co2eq, attribution_ratio, observed_at) "
"VALUES ('cheap', 'neuralwatt', 'coding_general', 100, 5.0e-05, 0.001, "
"2.4e-03, 0.25, ?)",
((now - timedelta(days=2)).isoformat(),),
)
conn.commit()
def _seed_verification(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO verifications (model_id, provider, kind, verdict, observed_at) "
"VALUES ('cheap', 'neuralwatt', 'structural', 'ok', ?)",
(_now().isoformat(),),
)
conn.commit()
def _seed_proficiency(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO proficiency (model_id, provider, category, blended_score, "
"source, last_updated) "
"VALUES ('cheap', 'neuralwatt', 'coding_general', 0.9, "
"'self_eval_thin', '2026-01-01T00:00:00+00:00')",
)
conn.commit()
@pytest.fixture
def seeded_client(tmp_path, monkeypatch):
"""A TestClient wired to a seeded temp DB, at /admin."""
conn = _make_db(tmp_path)
_seed_models(conn)
for _ in range(3):
_seed_decision(conn)
_seed_energy(conn)
_seed_verification(conn)
_seed_proficiency(conn)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_admin_health_returns_ok(seeded_client):
"""GET /admin/api/health returns 200 with {"status": "ok"}."""
resp = seeded_client.get("/admin/api/health")
assert resp.status_code == 200
assert resp.json() == {"status": "ok"}
def test_admin_snapshot_has_all_top_level_keys(seeded_client):
"""GET /admin/api/snapshot returns 200 with every required key."""
resp = seeded_client.get("/admin/api/snapshot")
assert resp.status_code == 200
data = resp.json()
for key in (
"quota",
"coverage",
"recent_decisions",
"per_model",
"verdict_mix",
"top_proficiency",
"health",
"generated_at",
):
assert key in data, f"missing top-level key {key!r}"
def test_admin_snapshot_health_has_expected_shape(seeded_client):
"""Snapshot's health block carries the same shape as /health."""
data = seeded_client.get("/admin/api/snapshot").json()
health = data["health"]
assert "status" in health
assert "counts" in health
assert "classifier_reachable" in health and isinstance(
health["classifier_reachable"], bool
)
assert "tiers" in health
assert "providers" in health
assert "api_keys_present" in health
def test_admin_snapshot_returns_seeded_data(seeded_client):
"""quota / per_model / verdict_mix / top_proficiency reflect the seed."""
data = seeded_client.get("/admin/api/snapshot").json()
assert data["quota"] is not None
assert isinstance(data["per_model"], list)
assert any(r["model_id"] == "cheap" for r in data["per_model"])
assert data["verdict_mix"]["ok"] == 1
assert isinstance(data["top_proficiency"], list)
assert data["top_proficiency"][0]["model_id"] == "cheap"
assert data["health"]["counts"]["models"] == 3
def test_admin_snapshot_contains_no_session_dir(seeded_client):
"""The JSON must never name session_dir or expose conversation text."""
body = seeded_client.get("/admin/api/snapshot").text
assert "session_dir" not in body
def test_admin_does_not_import_dispatcher():
"""Importing admin.py must not pull dispatcher into sys.modules."""
import subprocess
import sys
probe = (
"import admin; import sys; "
"assert 'dispatcher' not in sys.modules, "
"'import admin transitively imported dispatcher'"
)
subprocess.run(
[sys.executable, "-c", probe],
check=True,
cwd=str(ROOT),
capture_output=True,
)

249
tests/test_admin_history.py Normal file
View File

@@ -0,0 +1,249 @@
"""Tests for the /admin/api/history bucketed time-series endpoint.
``/api/history`` runs a bucketed GROUP BY over two existing tables
(``energy_observations`` and ``route_decisions``), keyed by a ``range`` query
param that selects a (span_seconds, bucket_seconds) pair. These tests drive a
real TestClient GET against a temp DB seeded at controlled timestamps, exactly
like ``test_admin_health.py`` — never a mock-call assertion.
"""
from __future__ import annotations
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(str(ROOT / "config.yaml"))
# The 1h bucket size used by range=24h; expected bucket ts are whole-second
# hour boundaries, computed the same way the SQL floors to a bucket index.
BUCKET_24H = 3600
def _make_db(tmp_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(tmp_path / "test.db"))
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA_SQL)
return conn
def _seed_models(conn: sqlite3.Connection) -> None:
for model_id, tier, context, cost, vision in (
("cheap", 2, 262128, 0.30, 1),
("dear", 2, 262128, 9.00, 0),
):
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, tier, context_window,
effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_vision, supports_json_mode,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, ?, ?, 192500, 16384, ?, ?,
?, 1, 'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(model_id, model_id, tier, context, cost, cost / 3, vision),
)
conn.commit()
def _seed_energy(
conn: sqlite3.Connection,
at: datetime,
energy_kwh: float,
cost_usd: float,
carbon_g_co2eq: float,
) -> None:
conn.execute(
"INSERT INTO energy_observations "
"(model_id, provider, task_category, completion_tokens, energy_kwh, "
"cost_usd, carbon_g_co2eq, attribution_ratio, observed_at) "
"VALUES ('cheap', 'neuralwatt', 'coding_general', 100, ?, ?, ?, 0.25, ?)",
(energy_kwh, cost_usd, carbon_g_co2eq, at.isoformat()),
)
def _seed_decision(conn: sqlite3.Connection, at: datetime) -> None:
conn.execute(
"""
INSERT INTO route_decisions (
observed_at, kind, task_category, task_tier, required_context_tokens,
confidence, classifier_ms, classification_source, latency_tolerance,
candidates_considered, selected_model, selected_provider,
runner_up_models, est_cost_usd, est_proficiency,
session_key, tools, images, json_mode, streamed,
flex_preference, flex_swapped, flex_forced
) VALUES (?, 'route', 'coding_general', 2, 100, 0.95, 200,
'classifier', 'interactive', 5, 'cheap', 'neuralwatt',
'[{"model_id":"dear","provider":"neuralwatt"}]',
0.001, 0.9, 'abc123', 0, 0, 0, 0,
'auto', 0, 1)
""",
(at.isoformat(),),
)
def _hour_floor(dt: datetime) -> datetime:
"""The start of the hour containing *dt*, on a whole second."""
return dt.replace(second=0, microsecond=0).replace(minute=0)
@pytest.fixture
def seeded_client(tmp_path, monkeypatch):
"""A TestClient at /admin wired to a temp DB.
Seeds, relative to the current hour, three energy rows across two
one-hour buckets and three decisions across the same two buckets — all
inside a 24h window:
- bucket A (current hour): R1 energy=0.001 cost=0.01 carbon=0.5,
R2 energy=0.002 cost=0.02 carbon=1.0
- bucket B (previous hour): R3 energy=0.004 cost=0.04 carbon=2.0
- decisions: D1+D2 in bucket A, D3 in bucket B
"""
conn = _make_db(tmp_path)
_seed_models(conn)
now = _now()
bucket_a = _hour_floor(now)
bucket_b = bucket_a - timedelta(hours=1)
_seed_energy(conn, bucket_a + timedelta(seconds=60), 0.001, 0.01, 0.5)
_seed_energy(conn, bucket_a + timedelta(seconds=120), 0.002, 0.02, 1.0)
_seed_energy(conn, bucket_b + timedelta(seconds=30), 0.004, 0.04, 2.0)
_seed_decision(conn, bucket_a + timedelta(seconds=60))
_seed_decision(conn, bucket_a + timedelta(seconds=200))
_seed_decision(conn, bucket_b + timedelta(seconds=30))
conn.commit()
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def _now() -> datetime:
return datetime.now(timezone.utc)
def _bucket_ts(dt: datetime) -> int:
"""The unix second of the 1h bucket containing *dt*."""
return int(dt.timestamp() // BUCKET_24H * BUCKET_24H)
def test_admin_history_24h_returns_series(seeded_client):
"""range=24h returns all five series, each a list of [ts, value] pairs."""
resp = seeded_client.get("/admin/api/history", params={"range": "24h"})
assert resp.status_code == 200
data = resp.json()
assert set(data) == {
"decisions_per_bucket",
"requests_per_bucket",
"cost_per_bucket",
"energy_per_bucket",
"carbon_per_bucket",
}
for series in data.values():
assert isinstance(series, list)
for point in series:
assert len(point) == 2
assert isinstance(point[0], (int, float))
def test_admin_history_24h_decisions_per_bucket(seeded_client):
"""decisions_per_bucket counts route_decisions per 1h bucket."""
now = _now()
bucket_a = _bucket_ts(_hour_floor(now))
bucket_b = bucket_a - BUCKET_24H
series = seeded_client.get("/admin/api/history", params={"range": "24h"}).json()[
"decisions_per_bucket"
]
assert series == [[bucket_b, 1], [bucket_a, 2]]
def test_admin_history_24h_requests_per_bucket(seeded_client):
"""requests_per_bucket counts energy_observations per 1h bucket."""
now = _now()
bucket_a = _bucket_ts(_hour_floor(now))
bucket_b = bucket_a - BUCKET_24H
series = seeded_client.get("/admin/api/history", params={"range": "24h"}).json()[
"requests_per_bucket"
]
assert series == [[bucket_b, 1], [bucket_a, 2]]
def test_admin_history_24h_cost_per_bucket(seeded_client):
"""cost_per_bucket sums cost_usd per 1h bucket."""
now = _now()
bucket_a = _bucket_ts(_hour_floor(now))
bucket_b = bucket_a - BUCKET_24H
series = seeded_client.get("/admin/api/history", params={"range": "24h"}).json()[
"cost_per_bucket"
]
assert series == [[bucket_b, 0.04], [bucket_a, 0.03]]
def test_admin_history_24h_energy_per_bucket(seeded_client):
"""energy_per_bucket sums energy_kwh per 1h bucket."""
now = _now()
bucket_a = _bucket_ts(_hour_floor(now))
bucket_b = bucket_a - BUCKET_24H
series = seeded_client.get("/admin/api/history", params={"range": "24h"}).json()[
"energy_per_bucket"
]
assert series == [[bucket_b, 0.004], [bucket_a, 0.003]]
def test_admin_history_24h_carbon_per_bucket(seeded_client):
"""carbon_per_bucket sums carbon_g_co2eq per 1h bucket."""
now = _now()
bucket_a = _bucket_ts(_hour_floor(now))
bucket_b = bucket_a - BUCKET_24H
series = seeded_client.get("/admin/api/history", params={"range": "24h"}).json()[
"carbon_per_bucket"
]
assert series == [[bucket_b, 2.0], [bucket_a, 1.5]]
def test_admin_history_bogus_range_returns_400(seeded_client):
"""An unlisted range value is rejected with 400."""
resp = seeded_client.get("/admin/api/history", params={"range": "bogus"})
assert resp.status_code == 400
@pytest.fixture
def empty_client(tmp_path, monkeypatch):
"""A TestClient at /admin wired to an empty (schema-only) temp DB."""
conn = _make_db(tmp_path)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_admin_history_empty_tables_return_empty_series(empty_client):
"""Empty tables yield 200 with every series as an empty list."""
resp = empty_client.get("/admin/api/history", params={"range": "24h"})
assert resp.status_code == 200
data = resp.json()
for series in data.values():
assert series == []

318
tests/test_admin_models.py Normal file
View File

@@ -0,0 +1,318 @@
"""Tests for the /admin/api/models read surfaces.
``admin.py`` mirrors ``metrics.py``'s contract — never import dispatcher, take
``(conn, cfg)`` explicitly — and is mounted under the ``/admin`` prefix. These
tests drive a real TestClient GET against the seeded temp DB, mirroring
``test_admin_health.py``'s ``seeded_client`` fixture.
The routes under test:
- GET /admin/api/models -> list of all models + proficiency
- GET /admin/api/models/{model_id}/{provider}-> single model detail (404 if absent)
- POST /admin/api/models/{model_id}/{provider}/availability -> upsert override
- DELETE /admin/api/models/{model_id}/{provider}/availability -> remove override
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(str(ROOT / "config.yaml"))
_ADMIN_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS admin_model_overrides (
model_id TEXT NOT NULL,
provider TEXT NOT NULL,
availability TEXT NOT NULL,
reason TEXT,
updated_at TEXT NOT NULL,
PRIMARY KEY (model_id, provider)
);
CREATE INDEX IF NOT EXISTS idx_admin_model_overrides_availability
ON admin_model_overrides (availability);
"""
def _make_db(tmp_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(tmp_path / "test.db"))
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA_SQL)
conn.executescript(_ADMIN_TABLE_SQL)
return conn
def _seed_models(conn: sqlite3.Connection, model_ids: tuple[str, ...]) -> None:
for model_id in model_ids:
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, display_name, tier,
context_window, effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_tools, supports_json_mode, supports_vision,
supports_reasoning, reasoning_default_enabled,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, ?, ?, ?, 192500, 16384,
?, ?, 1, 1, 1, 1, 1,
'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(
model_id,
model_id,
model_id,
2,
262128,
0.30,
0.10,
),
)
conn.commit()
def _seed_proficiency(
conn: sqlite3.Connection,
spec: tuple[tuple[str, str, float], ...],
) -> None:
"""Insert proficiency rows as (model_id, category, blended_score)."""
for model_id, category, blended in spec:
conn.execute(
"INSERT INTO proficiency (model_id, provider, category, "
"blended_score, source, last_updated) "
"VALUES (?, 'neuralwatt', ?, ?, 'self_eval_thin', "
"'2026-01-01T00:00:00+00:00')",
(model_id, category, blended),
)
conn.commit()
@pytest.fixture
def seeded_client(tmp_path, monkeypatch):
"""A TestClient wired to a seeded temp DB, at /admin."""
conn = _make_db(tmp_path)
_seed_models(conn, ("cheap", "dear", "tiny"))
_seed_proficiency(
conn,
(
("cheap", "coding_general", 0.9),
("cheap", "debugging", 0.85),
("dear", "coding_general", 0.95),
),
)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
@pytest.fixture
def empty_client(tmp_path, monkeypatch):
"""A TestClient over an empty DB (no models rows) at /admin."""
conn = _make_db(tmp_path)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_admin_models_returns_all_models_with_proficiency(seeded_client):
"""GET /admin/api/models returns one object per seeded model, each with a
category -> blended_score proficiency map."""
resp = seeded_client.get("/admin/api/models")
assert resp.status_code == 200
rows = resp.json()
assert isinstance(rows, list)
assert len(rows) == 3
by_id = {r["model_id"]: r for r in rows}
assert set(by_id) == {"cheap", "dear", "tiny"}
cheap = by_id["cheap"]
assert cheap["proficiency"] == {"coding_general": 0.9, "debugging": 0.85}
assert by_id["dear"]["proficiency"] == {"coding_general": 0.95}
assert by_id["tiny"]["proficiency"] == {}
def test_admin_models_row_shape(seeded_client):
"""Each model object carries every required scalar field."""
row = seeded_client.get("/admin/api/models").json()[0]
for key in (
"model_id",
"provider",
"base_model_id",
"display_name",
"availability",
"tier",
"context_window",
"effective_context_window",
"latency_class",
"reasoning_mode",
"context_variant",
"access_level",
"supports_tools",
"supports_json_mode",
"supports_vision",
"supports_reasoning",
"reasoning_default_enabled",
"cost_per_1m_prompt",
"cost_per_1m_completion",
"proficiency",
):
assert key in row, f"missing field {key!r}"
assert row["provider"] == "neuralwatt"
assert row["availability"] == "active"
assert row["tier"] == 2
assert row["context_window"] == 262128
assert row["supports_tools"] is True
assert row["supports_json_mode"] is True
assert row["supports_vision"] is True
assert row["supports_reasoning"] is True
assert row["reasoning_default_enabled"] is True
def test_admin_model_detail_returns_single_object(seeded_client):
"""GET /admin/api/models/{model_id}/{provider} returns one object, not a list."""
resp = seeded_client.get("/admin/api/models/cheap/neuralwatt")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict)
assert data["model_id"] == "cheap"
assert data["provider"] == "neuralwatt"
assert data["proficiency"] == {"coding_general": 0.9, "debugging": 0.85}
def test_admin_model_detail_unknown_model_returns_404(seeded_client):
"""A model_id that does not exist -> 404."""
resp = seeded_client.get("/admin/api/models/nope/neuralwatt")
assert resp.status_code == 404
def test_admin_model_detail_unknown_provider_returns_404(seeded_client):
"""A provider that does not exist for a known model -> 404."""
resp = seeded_client.get("/admin/api/models/cheap/nope")
assert resp.status_code == 404
def test_admin_models_empty_table_returns_empty_list(empty_client):
"""GET /admin/api/models with zero rows -> 200 + empty list."""
resp = empty_client.get("/admin/api/models")
assert resp.status_code == 200
assert resp.json() == []
def test_admin_models_never_expose_session_dir(seeded_client):
"""No response object may name session_dir or carry a prompt/conversation key."""
rows = seeded_client.get("/admin/api/models").json()
for row in rows:
for key in ("session_dir", "prompt", "conversation"):
assert key not in row, f"leaked {key!r} in model row"
def test_admin_model_detail_never_expose_session_dir(seeded_client):
"""The single-model JSON must never name session_dir or conversation keys."""
data = seeded_client.get("/admin/api/models/cheap/neuralwatt").json()
for key in ("session_dir", "prompt", "conversation"):
assert key not in data, f"leaked {key!r} in model detail"
# --- admin model overrides --------------------------------------------------
def test_post_override_sets_effective_availability_deprecated(seeded_client):
"""POST override -> effective_availability flips to deprecated, is_overridden
is True, raw availability stays 'active'."""
resp = seeded_client.post(
"/admin/api/models/cheap/neuralwatt/availability",
json={"availability": "deprecated", "reason": "failing verification"},
)
assert resp.status_code == 200
data = resp.json()
assert data["is_overridden"] is True
assert data["effective_availability"] == "deprecated"
assert data["availability"] == "active"
def test_post_override_sets_effective_availability_stale(seeded_client):
"""POST override with stale availability."""
resp = seeded_client.post(
"/admin/api/models/cheap/neuralwatt/availability",
json={"availability": "stale", "reason": "last seen long ago"},
)
assert resp.status_code == 200
data = resp.json()
assert data["is_overridden"] is True
assert data["effective_availability"] == "stale"
def test_post_override_invalid_availability_returns_422(seeded_client):
"""Bad availability value -> 422 validation error."""
resp = seeded_client.post(
"/admin/api/models/cheap/neuralwatt/availability",
json={"availability": "retired", "reason": "nope"},
)
assert resp.status_code == 422
def test_post_override_nonexistent_model_returns_404(seeded_client):
"""Trying to override a model that doesn't exist -> 404."""
resp = seeded_client.post(
"/admin/api/models/nonexistent/neuralwatt/availability",
json={"availability": "deprecated", "reason": "test"},
)
assert resp.status_code == 404
def test_delete_override_reverts_to_db_value(seeded_client):
"""POST then DELETE -> is_overridden becomes False, effective_availability
reverts to the DB value ('active')."""
# Set override
seeded_client.post(
"/admin/api/models/cheap/neuralwatt/availability",
json={"availability": "deprecated", "reason": "test"},
)
# Delete it
resp = seeded_client.delete(
"/admin/api/models/cheap/neuralwatt/availability"
)
assert resp.status_code == 200
data = resp.json()
assert data["is_overridden"] is False
assert data["effective_availability"] == "active"
assert data["availability"] == "active"
def test_model_list_reflects_effective_availability(seeded_client):
"""GET /admin/api/models includes effective_availability and is_overridden."""
seeded_client.post(
"/admin/api/models/cheap/neuralwatt/availability",
json={"availability": "deprecated", "reason": "test"},
)
rows = seeded_client.get("/admin/api/models").json()
by_id = {r["model_id"]: r for r in rows}
cheap = by_id["cheap"]
assert "effective_availability" in cheap
assert "is_overridden" in cheap
# dear and tiny should not be overridden
assert cheap["is_overridden"] is True
assert cheap["effective_availability"] == "deprecated"
assert by_id["dear"]["is_overridden"] is False

View File

@@ -0,0 +1,286 @@
"""Integration test: admin_model_overrides wire into routing.
Verifies that POST /admin/api/models/{model_id}/{provider}/availability
actually removes the model from /route candidates. The wired exclude set
(_admin_deprecated_models) must intersect with select_candidates'
exclude_models filter so the overridden model never appears in the
route response.
Uses the pattern from test_route_decisions.py: temp DB, seeded models with
energy + proficiency so a specific model WOULD win, then override + /route
assertion.
"""
from __future__ import annotations
import json
import sqlite3
import time
from pathlib import Path
from typing import Any
import pytest
from openai import OpenAI
from starlette.testclient import TestClient
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(str(ROOT / "config.yaml"))
ADMIN_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS admin_model_overrides (
model_id TEXT NOT NULL,
provider TEXT NOT NULL,
availability TEXT NOT NULL,
reason TEXT,
updated_at TEXT NOT NULL,
PRIMARY KEY (model_id, provider)
);
CREATE INDEX IF NOT EXISTS idx_admin_model_overrides_availability
ON admin_model_overrides (availability);
"""
# A model we will seed with best proficiency + energy so it WOULD be picked.
WINNER_MODEL = "premium"
# A second model that would be runner-up.
RUNNER_MODEL = "mid"
def _completion(model_id: str) -> dict:
"""An OpenAI-compatible completion body for a *fake* provider response."""
return {
"id": f"chatcmpl-{model_id}",
"object": "chat.completion",
"model": model_id,
"created": int(time.time()),
"choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "done"}}],
}
class FakeResponse:
"""Minimal fake for requests.post() and httpx.Response."""
def __init__(
self,
body: dict | None = None,
lines: list[str] | None = None,
headers: dict | None = None,
) -> None:
self.body = body or _completion("dummy")
self.lines = lines or []
self.headers = headers or {"content-type": "application/json"}
self.status_code = 200
@property
def text(self) -> str:
return json.dumps(self.body)
@property
def content(self) -> bytes:
return json.dumps(self.body).encode()
def json(self) -> dict:
return self.body
@pytest.fixture
def admin_override_router(tmp_path: Path, monkeypatch) -> tuple[TestClient, Path]:
"""A TestClient with a temp DB that has admin_model_overrides table,
seeded with two models (WINNER_MODEL and RUNNER_MODEL) where WINNER
has best proficiency + energy."""
import admin
db_path = tmp_path / "test.db"
conn = sqlite3.connect(str(db_path))
conn.executescript(SCHEMA_SQL)
conn.executescript(ADMIN_TABLE_SQL)
dispatcher.ensure_route_decisions(conn)
# Seed two models: WINNER (tier 2, best) and RUNNER (tier 2).
for mid, cost_prompt, cost_compl, prof_score in [
(WINNER_MODEL, 0.50, 0.30, 0.95),
(RUNNER_MODEL, 0.30, 0.15, 0.60),
]:
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, display_name, tier,
context_window, effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_tools, supports_json_mode, supports_vision,
supports_reasoning, reasoning_default_enabled,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, ?, ?, 262128, 192500, 16384,
?, ?, 1, 1, 1, 1, 1,
'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(
mid,
mid,
mid,
2,
cost_prompt,
cost_compl,
),
)
# Seed proficiency: WINNER has the best score for coding_general.
conn.execute(
"INSERT INTO proficiency (model_id, provider, category, "
"blended_score, source, last_updated) "
"VALUES (?, 'neuralwatt', 'coding_general', ?, 'self_eval_thin', "
"'2026-01-01T00:00:00+00:00')",
(WINNER_MODEL, 0.95),
)
conn.execute(
"INSERT INTO proficiency (model_id, provider, category, "
"blended_score, source, last_updated) "
"VALUES (?, 'neuralwatt', 'coding_general', ?, 'self_eval_thin', "
"'2026-01-01T00:00:00+00:00')",
(RUNNER_MODEL, 0.60),
)
# Seed one energy observation so scoring works.
now = "2026-08-22T00:00:00+00:00"
for mid in (WINNER_MODEL, RUNNER_MODEL):
conn.execute(
"""
INSERT INTO energy_observations (
model_id, provider, prompt_tokens, completion_tokens,
energy_kwh, carbon_g_co2eq, cost_usd, observed_at
) VALUES (?, 'neuralwatt', 200, 500, 0.005, 2.0, 0.10, ?)
""",
(mid, now),
)
conn.commit()
conn.close()
# Redirect the dispatcher to the temp DB.
monkeypatch.setattr(dispatcher.cfg.database, "path", str(db_path))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.freshness, "exclude_stale", True)
monkeypatch.setattr(dispatcher.cfg.freshness, "exclude_deprecated", True)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setattr(dispatcher.cfg.logging, "log_route_decisions", True)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
# Ensure admin tables are wired into the startup migration.
from admin import ensure_admin_tables
temp_conn = sqlite3.connect(str(db_path))
temp_conn.row_factory = sqlite3.Row
try:
ensure_admin_tables(conn=temp_conn)
except Exception:
pass # may already exist
finally:
temp_conn.close()
# Stub the classifier — always returns coding_general, tier 2.
monkeypatch.setattr(
dispatcher,
"classify",
lambda task, context: dispatcher.Classification(
task_category="coding_general",
task_tier=2,
required_context_tokens=100,
confidence=0.9,
),
)
# Stub the provider call so the /route endpoint doesn't actually call
# NeuralWatt. It just returns a minimal response.
def fake_post(url: str, headers: Any = None, json: Any = None,
stream: bool = False, timeout: int = 600):
if stream:
return FakeResponse(lines=["data: ..."])
return FakeResponse(_completion(json["model"] if json else "dummy"))
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
app = dispatcher.app
with TestClient(app) as client:
yield client, db_path
def test_admin_override_excludes_model_from_route(admin_override_router):
"""When an admin override marks WINNER_MODEL as deprecated, POST /route
must NOT pick it. The selected model should be RUNNER_MODEL instead,
and candidates_considered should be 1 (not 2).
This is the CRITICAL integration test: override → router exclusion.
"""
client, db_path = admin_override_router
# First, verify that WITHOUT an override, the router picks WINNER.
resp = client.post("/route", json={"task": "write a python function"})
assert resp.status_code == 200
body = resp.json()
assert body["selected"]["model_id"] == WINNER_MODEL
assert body["candidates_considered"] == 2
# Now mark WINNER as deprecated via admin override.
override_resp = client.post(
f"/admin/api/models/{WINNER_MODEL}/neuralwatt/availability",
json={"availability": "deprecated", "reason": "failing verification"},
)
assert override_resp.status_code == 200
override_data = override_resp.json()
assert override_data["is_overridden"] is True
assert override_data["effective_availability"] == "deprecated"
# POST /route again: the router must NOT pick the overridden model.
resp = client.post("/route", json={"task": "write a python function"})
assert resp.status_code == 200
body = resp.json()
assert body["selected"]["model_id"] == RUNNER_MODEL
assert body["candidates_considered"] == 1
# Verify the override model is in the excluded set via the decision log.
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
decision_row = conn.execute(
"SELECT * FROM route_decisions ORDER BY id DESC LIMIT 1"
).fetchone()
conn.close()
assert decision_row["selected_model"] == RUNNER_MODEL
def test_admin_override_revert_includes_model_again(admin_override_router):
"""After DELETE on the admin override, the model must become routable
again and be picked if it still has best scores."""
client, db_path = admin_override_router
# Mark as deprecated.
client.post(
f"/admin/api/models/{WINNER_MODEL}/neuralwatt/availability",
json={"availability": "deprecated", "reason": "test"},
)
# Verify route picks RUNNER.
resp = client.post("/route", json={"task": "write a python function"})
assert resp.json()["selected"]["model_id"] == RUNNER_MODEL
# Delete override.
delete_resp = client.delete(
f"/admin/api/models/{WINNER_MODEL}/neuralwatt/availability"
)
assert delete_resp.status_code == 200
del_data = delete_resp.json()
assert del_data["is_overridden"] is False
# Route again: WINNER should be back on the radar.
resp = client.post("/route", json={"task": "write a python function"})
assert resp.status_code == 200
body = resp.json()
assert body["selected"]["model_id"] == WINNER_MODEL
assert body["candidates_considered"] == 2

180
tests/test_admin_runtime.py Normal file
View File

@@ -0,0 +1,180 @@
"""Tests for the /admin/api runtime config toggle endpoints.
``admin.py`` exposes GET /admin/api/runtime (persisted + in-memory state for
each toggle knob) and POST /admin/api/runtime/{knob} (flip the in-memory cfg
value only, never config.yaml). These tests drive a real TestClient against the
seeded temp DB, mirroring ``test_admin_health.py``'s ``seeded_client`` fixture.
The key contract under test: a POST flips the RUNTIME value but must leave the
PERSISTED (config.yaml) value untouched.
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(str(ROOT / "config.yaml"))
def _make_db(tmp_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(tmp_path / "test.db"))
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA_SQL)
return conn
def _seed_models(conn: sqlite3.Connection) -> None:
for model_id, tier, context, cost, vision in (
("cheap", 2, 262128, 0.30, 1),
("dear", 2, 262128, 9.00, 0),
("tiny", 1, 131072, 0.10, 1),
):
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, tier, context_window,
effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_vision, supports_json_mode,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, ?, ?, 192500, 16384, ?, ?,
?, 1, 'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(model_id, model_id, tier, context, cost, cost / 3, vision),
)
conn.commit()
@pytest.fixture
def seeded_client(tmp_path, monkeypatch):
"""A TestClient wired to a seeded temp DB, at /admin."""
conn = _make_db(tmp_path)
_seed_models(conn)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_runtime_GET_reports_every_knob(seeded_client):
"""GET /admin/api/runtime reports persisted + runtime for all 8 knobs."""
resp = seeded_client.get("/admin/api/runtime")
assert resp.status_code == 200
body = resp.json()
for knob in (
"log_route_decisions",
"log_energy_observations",
"circuit_breaker",
"local_llm_enabled",
"session_cache_enabled",
"pinch_enabled",
"pinch_relevance_enabled",
"default_flex_preference",
):
assert knob in body
assert set(body[knob]) == {"persisted", "runtime"}
# The runtime value mirrors the live dispatcher.cfg for the boolean knobs;
# the persisted value mirrors config.yaml.
assert body["log_route_decisions"]["runtime"] == dispatcher.cfg.logging.log_route_decisions
assert body["log_route_decisions"]["persisted"] == CFG.logging.log_route_decisions
assert body["circuit_breaker"] == {
"persisted": {"enabled": CFG.circuit_breaker.enabled},
"runtime": {"enabled": dispatcher.cfg.circuit_breaker.enabled},
}
assert body["default_flex_preference"] == {
"persisted": CFG.routing.default_flex_preference.value,
"runtime": dispatcher.cfg.routing.default_flex_preference.value,
}
def test_toggle_log_route_decisions_flips_runtime_not_persisted(
seeded_client, monkeypatch
):
"""POST a boolean knob flips the runtime value; config.yaml is untouched."""
# Guard shared global state: record the original so it restores after this
# test, since the POST mutates dispatcher.cfg in place.
monkeypatch.setattr(
dispatcher.cfg.logging,
"log_route_decisions",
dispatcher.cfg.logging.log_route_decisions,
)
initial = seeded_client.get("/admin/api/runtime").json()
assert initial["log_route_decisions"]["runtime"] is True
resp = seeded_client.post(
"/admin/api/runtime/log_route_decisions", json={"value": False}
)
assert resp.status_code == 200
after = seeded_client.get("/admin/api/runtime").json()
# Runtime flipped ...
assert after["log_route_decisions"]["runtime"] is False
# ... but persisted is unchanged and still matches config.yaml.
assert after["log_route_decisions"]["persisted"] == CFG.logging.log_route_decisions
assert after["log_route_decisions"]["persisted"] == initial["log_route_decisions"][
"persisted"
]
def test_post_invalid_flex_value_returns_422(seeded_client):
"""A flex preference outside the 4 allowed values is rejected with 422."""
resp = seeded_client.post(
"/admin/api/runtime/default_flex_preference", json={"value": "banana"}
)
assert resp.status_code == 422
# The in-memory value must not have changed.
assert (
seeded_client.get("/admin/api/runtime").json()["default_flex_preference"][
"runtime"
]
== "auto"
)
def test_post_valid_flex_value_updates_runtime(seeded_client, monkeypatch):
"""A valid flex preference is accepted and reflected in runtime state."""
monkeypatch.setattr(
dispatcher.cfg.routing,
"default_flex_preference",
dispatcher.cfg.routing.default_flex_preference,
)
resp = seeded_client.post(
"/admin/api/runtime/default_flex_preference", json={"value": "force-flex"}
)
assert resp.status_code == 200
assert seeded_client.get("/admin/api/runtime").json()["default_flex_preference"][
"runtime"
] == "force-flex"
def test_post_unknown_knob_returns_400(seeded_client):
"""POSTing an unknown knob name is rejected with 400."""
resp = seeded_client.post(
"/admin/api/runtime/not_a_real_knob", json={"value": True}
)
assert resp.status_code == 400
def test_post_non_boolean_for_bool_knob_returns_422(seeded_client):
"""POSTing a non-boolean value for a boolean knob is rejected with 422."""
resp = seeded_client.post(
"/admin/api/runtime/session_cache_enabled", json={"value": "yes"}
)
assert resp.status_code == 422

View File

@@ -0,0 +1,176 @@
"""Tests for GET /admin/api/snapshot: the full top-level key contract.
The snapshot is the admin dashboard's main data payload. This file locks the
complete key set (not just a hand-picked subset) so a dropped or renamed key
fails the suite instead of silently missing from the UI. It drives a real
TestClient against a seeded temp DB, mirroring ``test_admin_health.py``.
"""
from __future__ import annotations
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(str(ROOT / "config.yaml"))
EXHAUSTIVE_KEYS = (
"quota",
"coverage",
"recent_decisions",
"per_model",
"verdict_mix",
"top_proficiency",
"health",
"generated_at",
)
def _now() -> datetime:
return datetime.now(timezone.utc)
def _make_db(tmp_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(tmp_path / "test.db"))
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA_SQL)
return conn
def _seed_models(conn: sqlite3.Connection) -> None:
for model_id, tier, context, cost, vision in (
("cheap", 2, 262128, 0.30, 1),
("dear", 2, 262128, 9.00, 0),
):
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, tier, context_window,
effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_vision, supports_json_mode,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, ?, ?, 192500, 16384, ?, ?,
?, 1, 'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(model_id, model_id, tier, context, cost, cost / 3, vision),
)
conn.commit()
def _seed_decision(conn: sqlite3.Connection) -> None:
conn.execute(
"""
INSERT INTO route_decisions (
observed_at, kind, task_category, task_tier, required_context_tokens,
confidence, classifier_ms, classification_source, latency_tolerance,
candidates_considered, selected_model, selected_provider,
runner_up_models, est_cost_usd, est_proficiency,
session_key, tools, images, json_mode, streamed,
flex_preference, flex_swapped, flex_forced
) VALUES (?, 'route', 'coding_general', 2, 100, 0.95, 200,
'classifier', 'interactive', 5, 'cheap', 'neuralwatt',
'[{"model_id":"dear","provider":"neuralwatt"}]',
0.001, 0.9, 'abc123', 0, 0, 0, 0,
'auto', 0, 1)
""",
(_now().isoformat(),),
)
conn.commit()
def _seed_energy(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO energy_observations "
"(model_id, provider, task_category, completion_tokens, energy_kwh, "
"cost_usd, carbon_g_co2eq, attribution_ratio, observed_at) "
"VALUES ('cheap', 'neuralwatt', 'coding_general', 100, 5.0e-05, 0.001, "
"2.4e-03, 0.25, ?)",
((_now() - timedelta(days=2)).isoformat(),),
)
conn.commit()
def _seed_verification(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO verifications (model_id, provider, kind, verdict, observed_at) "
"VALUES ('cheap', 'neuralwatt', 'structural', 'ok', ?)",
(_now().isoformat(),),
)
conn.commit()
def _seed_proficiency(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO proficiency (model_id, provider, category, blended_score, "
"source, last_updated) "
"VALUES ('cheap', 'neuralwatt', 'coding_general', 0.9, "
"'self_eval_thin', '2026-01-01T00:00:00+00:00')",
)
conn.commit()
@pytest.fixture
def seeded_client(tmp_path, monkeypatch):
conn = _make_db(tmp_path)
_seed_models(conn)
for _ in range(3):
_seed_decision(conn)
_seed_energy(conn)
_seed_verification(conn)
_seed_proficiency(conn)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_admin_snapshot_returns_exhaustive_top_level_keys(seeded_client):
"""Every documented top-level snapshot key is present in the response."""
resp = seeded_client.get("/admin/api/snapshot")
assert resp.status_code == 200
data = resp.json()
for key in EXHAUSTIVE_KEYS:
assert key in data, f"missing top-level snapshot key {key!r}"
def test_admin_snapshot_generated_at_is_iso(seeded_client):
"""generated_at is an ISO-8601 timestamp and freshly generated."""
data = seeded_client.get("/admin/api/snapshot").json()
generated = datetime.fromisoformat(data["generated_at"])
assert generated.tzinfo is not None
delta = abs((_now() - generated).total_seconds())
assert delta < 60, "snapshot generated_at is stale"
def test_admin_snapshot_empty_db_still_returns_all_keys(tmp_path, monkeypatch):
"""An unseeded DB returns the same key set, with empty collections."""
conn = _make_db(tmp_path)
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
data = client.get("/admin/api/snapshot").json()
for key in EXHAUSTIVE_KEYS:
assert key in data, f"empty-DB snapshot missing key {key!r}"
assert data["per_model"] == []
assert data["recent_decisions"] == []
assert data["top_proficiency"] == []

View File

@@ -0,0 +1,194 @@
"""Tests for the /admin/api operational trigger endpoints.
refresh-catalog, seed-energy, and apply-feedback run repo maintenance scripts
via ``asyncio.create_subprocess_exec``; restart-service schedules systemctl
through FastAPI BackgroundTasks. These tests monkeypatch
``asyncio.create_subprocess_exec`` (and ``subprocess.run``) so no real process
or network is ever touched — they assert the endpoint maps query params and
CLI args onto the spawned command and returns the documented job shape.
"""
from __future__ import annotations
import sqlite3
import sys
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import admin
import dispatcher
from config import load_config
ROOT = Path(__file__).resolve().parent.parent
CFG = load_config(str(ROOT / "config.yaml"))
class FakeProcess:
"""A stand-in for ``asyncio.subprocess.Process`` with a known outcome."""
def __init__(self, stdout: bytes = b"ok\n", returncode: int = 0):
self._stdout = stdout
self.returncode = returncode
self.killed = False
async def communicate(self):
return self._stdout, None
async def wait(self):
return self.returncode
def kill(self):
self.killed = True
class _Recorder:
"""Captures every ``create_subprocess_exec`` call as ``(args, kwargs)``."""
def __init__(self):
self.calls: list[tuple[tuple, dict]] = []
@pytest.fixture
def fake_spawn(monkeypatch):
"""Replace ``asyncio.create_subprocess_exec`` with a success-bound fake."""
recorder = _Recorder()
async def _fake(*args, **kwargs):
recorder.calls.append((list(args), kwargs))
return FakeProcess(stdout=b"ok\n", returncode=0)
monkeypatch.setattr(admin.asyncio, "create_subprocess_exec", _fake)
return recorder
@pytest.fixture
def failing_spawn(monkeypatch):
"""A fake that makes any spawned command fail with returncode 3."""
recorder = _Recorder()
async def _fake(*args, **kwargs):
recorder.calls.append((list(args), kwargs))
return FakeProcess(stdout=b"boom\n", returncode=3)
monkeypatch.setattr(admin.asyncio, "create_subprocess_exec", _fake)
return recorder
@pytest.fixture
def seeded_client(tmp_path, monkeypatch):
"""A TestClient wired to dispatcher.app with a temp DB (mounts /admin)."""
conn = sqlite3.connect(str(tmp_path / "test.db"))
conn.executescript((ROOT / "schema.sql").read_text())
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
# --- refresh-catalog --------------------------------------------------------
def test_refresh_catalog_runs_poller_then_tier(seeded_client, fake_spawn):
"""POST /admin/api/refresh-catalog spawns poller then tier, &&-semantics."""
resp = seeded_client.post("/admin/api/refresh-catalog")
assert resp.status_code == 200
job = resp.json()
assert job["status"] == "success"
assert job["returncode"] == 0
assert "poller.py" in job["command"]
assert "tier.py" in job["command"]
# One spawn per step, in order, with the venv python as the interpreter.
argv = [args for args, _ in fake_spawn.calls]
assert argv == [
[sys.executable, "poller.py"],
[sys.executable, "tier.py"],
]
for _, kwargs in fake_spawn.calls:
assert kwargs["cwd"] == str(ROOT)
def test_refresh_catalog_job_shape(seeded_client, fake_spawn):
"""The job object carries id/command/status/returncode/output_tail."""
job = seeded_client.post("/admin/api/refresh-catalog").json()
assert isinstance(job["id"], str) and job["id"]
assert "command" in job
assert job["status"] == "success"
assert job["returncode"] == 0
# Two steps each emit "ok\n", so the merged tail carries both.
assert job["output_tail"] == "ok\nok\n"
def test_failed_command_reports_failure(seeded_client, failing_spawn):
"""A non-zero returncode is reported as failed with that returncode."""
job = seeded_client.post("/admin/api/apply-feedback").json()
assert job["status"] == "failed"
assert job["returncode"] == 3
assert job["output_tail"] == "boom\n"
# --- seed-energy ------------------------------------------------------------
def test_seed_energy_default_samples(seeded_client, fake_spawn):
"""Without ?samples=, seed-energy defaults to ``--samples 5``."""
seeded_client.post("/admin/api/seed-energy")
argv = fake_spawn.calls[0][0]
assert argv == [sys.executable, "seed_energy.py", "--samples", "5"]
def test_seed_energy_accepts_samples_query(seeded_client, fake_spawn):
"""POST /admin/api/seed-energy?samples=3 forwards ``--samples 3``."""
resp = seeded_client.post("/admin/api/seed-energy?samples=3")
assert resp.status_code == 200
assert resp.json()["status"] == "success"
argv = fake_spawn.calls[0][0]
assert argv == [sys.executable, "seed_energy.py", "--samples", "3"]
# --- apply-feedback ---------------------------------------------------------
def test_apply_feedback_apply_mode(seeded_client, fake_spawn):
"""Without dry_run, feedback.py runs with no extra flag."""
resp = seeded_client.post("/admin/api/apply-feedback")
assert resp.status_code == 200
assert resp.json()["status"] == "success"
argv = fake_spawn.calls[0][0]
assert argv == [sys.executable, "feedback.py"]
def test_apply_feedback_dry_run(seeded_client, fake_spawn):
"""?dry_run=true appends ``--dry-run`` to the feedback command."""
seeded_client.post("/admin/api/apply-feedback?dry_run=true")
argv = fake_spawn.calls[0][0]
assert argv == [sys.executable, "feedback.py", "--dry-run"]
# --- restart-service --------------------------------------------------------
def test_restart_service_returns_immediately(seeded_client, monkeypatch):
"""POST /admin/api/restart-service returns {"status": "restarting"} 200.
The systemctl call is scheduled via BackgroundTasks, never awaited inline,
so the response body is the immediate "restarting" status and the actual
restart fires as a post-response background task.
"""
calls: list[tuple] = []
monkeypatch.setattr(
admin.subprocess, "run", lambda *a, **k: calls.append((a, k))
)
resp = seeded_client.post("/admin/api/restart-service")
assert resp.status_code == 200
assert resp.json() == {"status": "restarting"}
# The BackgroundTask ran after the response was produced; it must have
# scheduled exactly the systemctl restart command.
assert calls, "BackgroundTask never fired systemctl"
spawned = calls[0][0][0]
assert spawned == ["systemctl", "--user", "restart", "llm-router.service"]