diff --git a/README.md b/README.md index 1fc9211efb..c9a5a4b560 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,8 @@ fences, including unclosed fences, in complete and streamed replies. Optional per-model pricing must use one currency across all priced models. DeerFlow disables Console cost estimates when currencies are mixed rather than presenting an invalid aggregate. + For older runs without a per-model usage breakdown, Console attributes the + recorded total and input token counts to the run's model. Administrators can open **Settings → Models** to select a provider and enter only its **API Key** and **model ID**. Endpoints, authentication and adapters are diff --git a/backend/app/gateway/routers/AGENTS.md b/backend/app/gateway/routers/AGENTS.md index 91f0f0088b..6697d8c3f6 100644 --- a/backend/app/gateway/routers/AGENTS.md +++ b/backend/app/gateway/routers/AGENTS.md @@ -1,5 +1,10 @@ # Router contracts +Console usage attributes legacy rows without a per-model usage map to +`model_name`, including their recorded `total_input_tokens`. Accumulate those +inputs with newer usage buckets for the same model; do not infer cache hits. +Regression coverage lives in `tests/test_console_router.py`. + Shared scheduled-task update validation preserves an unchanged future `once` instant on a live task inside `scheduler.min_once_delay_seconds`; new/changed instants and terminal reactivation still enforce the minimum delay. diff --git a/backend/app/gateway/routers/console.py b/backend/app/gateway/routers/console.py index f03b550d39..8aedc75394 100644 --- a/backend/app/gateway/routers/console.py +++ b/backend/app/gateway/routers/console.py @@ -528,6 +528,7 @@ async def console_usage( # Legacy rows predating token_usage_by_model: fall back to the run's model. entry = by_model.setdefault(row.model_name, ConsoleUsageModelBreakdown()) entry.tokens += run_tokens + entry.input_tokens += row.total_input_tokens or 0 entry.runs += 1 if run_cost is not None: entry.cost = round((entry.cost or 0.0) + run_cost, 6) diff --git a/backend/tests/test_console_router.py b/backend/tests/test_console_router.py index 45d1a739a2..1fafdfdba4 100644 --- a/backend/tests/test_console_router.py +++ b/backend/tests/test_console_router.py @@ -234,6 +234,30 @@ class TestConsoleUsage: assert data["by_model"]["qwen"]["tokens"] == 70 # Legacy row (empty token_usage_by_model) falls back to model_name assert data["by_model"]["gpt-x"]["tokens"] == 50 + assert data["by_model"]["gpt-x"]["input_tokens"] == 50 + + @pytest.mark.parametrize("legacy_usage", [{}, None], ids=["empty-map", "null-map"]) + @pytest.mark.parametrize("legacy_input_tokens", [0, 50]) + def test_legacy_input_tokens_accumulate_with_model_usage(self, client, session_factory, legacy_usage, legacy_input_tokens): + async def _update_legacy_run() -> None: + async with session_factory() as session: + row = await session.get(RunRow, "r3") + row.model_name = "minimax-m2" + row.token_usage_by_model = legacy_usage + row.total_input_tokens = legacy_input_tokens + row.total_output_tokens = 50 - legacy_input_tokens + await session.commit() + + asyncio.run(_update_legacy_run()) + response = client.get("/api/console/usage", params={"days": 14}) + assert response.status_code == 200 + data = response.json() + model = data["by_model"]["minimax-m2"] + assert model["input_tokens"] == 1000 + legacy_input_tokens + assert model["tokens"] == 1550 + assert model["runs"] == 3 + assert model["cache_read_tokens"] == 500 + assert sum(day["input_tokens"] for day in data["days"]) == sum(entry["input_tokens"] for entry in data["by_model"].values()) def test_window_excludes_old_rows_but_stats_include_them(self, client): usage = client.get("/api/console/usage", params={"days": 7}).json()