neuralwatt-router-service #8
60
CLAUDE.md
60
CLAUDE.md
@@ -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 —
|
||||
|
||||
68
README.md
68
README.md
@@ -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
802
admin.py
Normal 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
|
||||
0
admin/frontend/__init__.py
Normal file
0
admin/frontend/__init__.py
Normal file
916
admin/frontend/index.html
Normal file
916
admin/frontend/index.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
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
23
admin_schema.sql
Normal 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
166
assets/6krrt-logo.svg
Normal 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 |
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
133
code_plans/.playwright-mcp/console-2026-08-29T14-42-18-565Z.log
Normal file
133
code_plans/.playwright-mcp/console-2026-08-29T14-42-18-565Z.log
Normal 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
|
||||
164
code_plans/.playwright-mcp/console-2026-08-29T14-47-56-594Z.log
Normal file
164
code_plans/.playwright-mcp/console-2026-08-29T14-47-56-594Z.log
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
756
code_plans/.playwright-mcp/page-2026-08-29T06-11-19-085Z.yml
Normal file
756
code_plans/.playwright-mcp/page-2026-08-29T06-11-19-085Z.yml
Normal 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]
|
||||
851
code_plans/.playwright-mcp/page-2026-08-29T06-11-46-660Z.yml
Normal file
851
code_plans/.playwright-mcp/page-2026-08-29T06-11-46-660Z.yml
Normal 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
|
||||
95
code_plans/.playwright-mcp/page-2026-08-29T07-07-40-410Z.yml
Normal file
95
code_plans/.playwright-mcp/page-2026-08-29T07-07-40-410Z.yml
Normal 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]
|
||||
725
code_plans/.playwright-mcp/page-2026-08-29T07-17-18-936Z.yml
Normal file
725
code_plans/.playwright-mcp/page-2026-08-29T07-17-18-936Z.yml
Normal 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]
|
||||
95
code_plans/.playwright-mcp/page-2026-08-29T14-42-18-860Z.yml
Normal file
95
code_plans/.playwright-mcp/page-2026-08-29T14-42-18-860Z.yml
Normal 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]
|
||||
812
code_plans/.playwright-mcp/page-2026-08-29T14-42-58-029Z.yml
Normal file
812
code_plans/.playwright-mcp/page-2026-08-29T14-42-58-029Z.yml
Normal 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]
|
||||
812
code_plans/.playwright-mcp/page-2026-08-29T14-44-04-172Z.yml
Normal file
812
code_plans/.playwright-mcp/page-2026-08-29T14-44-04-172Z.yml
Normal 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]
|
||||
581
code_plans/.playwright-mcp/page-2026-08-29T14-47-57-000Z.yml
Normal file
581
code_plans/.playwright-mcp/page-2026-08-29T14-47-57-000Z.yml
Normal 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]
|
||||
812
code_plans/.playwright-mcp/page-2026-08-29T14-48-31-579Z.yml
Normal file
812
code_plans/.playwright-mcp/page-2026-08-29T14-48-31-579Z.yml
Normal 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]
|
||||
581
code_plans/.playwright-mcp/page-2026-08-29T15-00-11-987Z.yml
Normal file
581
code_plans/.playwright-mcp/page-2026-08-29T15-00-11-987Z.yml
Normal 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]
|
||||
95
code_plans/.playwright-mcp/page-2026-08-29T15-01-33-298Z.yml
Normal file
95
code_plans/.playwright-mcp/page-2026-08-29T15-01-33-298Z.yml
Normal 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]
|
||||
BIN
code_plans/.playwright-mcp/task-8-admin-visual-fixes-v2-full.png
Normal file
BIN
code_plans/.playwright-mcp/task-8-admin-visual-fixes-v2-full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 348 KiB |
BIN
code_plans/.playwright-mcp/task-8-admin-visual-fixes-v2.png
Normal file
BIN
code_plans/.playwright-mcp/task-8-admin-visual-fixes-v2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
3
code_plans/.playwright-mcp/task-8-v2-console-clean.log
Normal file
3
code_plans/.playwright-mcp/task-8-v2-console-clean.log
Normal 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
|
||||
3
code_plans/.playwright-mcp/task-8-v2-console-final.log
Normal file
3
code_plans/.playwright-mcp/task-8-v2-console-final.log
Normal 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
|
||||
3
code_plans/.playwright-mcp/task-8-v2-console-full.log
Normal file
3
code_plans/.playwright-mcp/task-8-v2-console-full.log
Normal 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
|
||||
BIN
code_plans/admin_dashboard.png
Normal file
BIN
code_plans/admin_dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
210
code_plans/router-unreachable-signal-investigation.md
Normal file
210
code_plans/router-unreachable-signal-investigation.md
Normal 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.
|
||||
BIN
code_plans/task-8-admin-visual-fixes-post-save.png
Normal file
BIN
code_plans/task-8-admin-visual-fixes-post-save.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 599 KiB |
BIN
code_plans/task-8-admin-visual-fixes.png
Normal file
BIN
code_plans/task-8-admin-visual-fixes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 601 KiB |
116
code_reviews/admin-visual-fixes-review.md
Normal file
116
code_reviews/admin-visual-fixes-review.md
Normal 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.
|
||||
132
code_reviews/pinch-relevance-and-failover-review.md
Normal file
132
code_reviews/pinch-relevance-and-failover-review.md
Normal 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.
|
||||
125
code_reviews/resolve-review-findings-sse-lock-gap-review.md
Normal file
125
code_reviews/resolve-review-findings-sse-lock-gap-review.md
Normal 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.
|
||||
183
code_reviews/router-admin-portal-implementation-review.md
Normal file
183
code_reviews/router-admin-portal-implementation-review.md
Normal 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.
|
||||
215
code_reviews/router-admin-portal-plan-review.md
Normal file
215
code_reviews/router-admin-portal-plan-review.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
191
documentation_plans/readme-restructure.md
Normal file
191
documentation_plans/readme-restructure.md
Normal 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.
|
||||
102
documentation_reviews/readme-restructure-review.md
Normal file
102
documentation_reviews/readme-restructure-review.md
Normal 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
187
tests/test_admin_config.py
Normal 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() != ""
|
||||
|
||||
97
tests/test_admin_frontend.py
Normal file
97
tests/test_admin_frontend.py
Normal 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
208
tests/test_admin_health.py
Normal 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
249
tests/test_admin_history.py
Normal 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
318
tests/test_admin_models.py
Normal 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
|
||||
|
||||
286
tests/test_admin_routing_override.py
Normal file
286
tests/test_admin_routing_override.py
Normal 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
180
tests/test_admin_runtime.py
Normal 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
|
||||
176
tests/test_admin_snapshot.py
Normal file
176
tests/test_admin_snapshot.py
Normal 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"] == []
|
||||
194
tests/test_admin_triggers.py
Normal file
194
tests/test_admin_triggers.py
Normal 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"]
|
||||
Reference in New Issue
Block a user