mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(tg): cockpit data correctness — real GLM pricing, display timezone, agent activity tracking (#666)
* fix(tg): cockpit data correctness — real GLM pricing, display timezone, agent activity tracking
Three root causes behind the Mini App/bot showing wrong numbers:
Pricing: glm-5.2 gets a grounded per-token rate (z.ai published pricing,
$1.40/$4.40/$0.26 per 1M, source+date in the table comment) so a GLM
fleet day stops reporting $0.00 for half a million tokens; ungrounded
Ollama-Cloud models render "subscription (untracked)" instead of a bare
zero (is_ollama_cloud_model, consumed directly by the cockpit). Side
effect, intended and documented: honestly-priced GLM now trips the
downgrade-only comparator for new qa/documenter complexity pins.
Display timezone: the cockpit bucketed days in UTC for a GMT+2 operator.
New pure foundation module display_time (resolve_zone/local_date/
trailing_dates/day_bounds_utc, DST-correct with tests for the 23h/25h
days) + ROBOCO_DISPLAY_TIMEZONE (IANA-validated, default UTC); the
cockpit's spend/velocity series bucket raw session/completion rows by
the display zone. The UTC-keyed rollup table and the main dashboard are
deliberately untouched.
Agent activity: AgentTable.status was never set to ACTIVE and
current_task_id was never written anywhere — "active: 0, working: []"
was structurally permanent. Every claim path now marks the claimant
ACTIVE with rollback symmetry (_finalize_claim for dev/PM claims,
_qa_or_doc_claim for QA/doc/PR-gate claims, pr_review_claim for external
review) and every release path clears it (pass/fail QA, pr_pass/pr_fail,
complete_review, advance-to-PM-review, reaper unclaim, voluntary
unclaim, reassign retarget, pool divert, admin transitions, unblock
restore-to-in-progress). The bot's /status shares the cockpit's fleet
derivation so the two surfaces can't disagree. Known ceiling, commented:
one current_task_id column shows a multi-root coordinator PM's most
recent claim only.
Drill: sonnet develop -> sonnet adversarial (refuted the original
chokepoint coverage claim; QA/doc/reviewer paths were unwired) ->
correction round (wired them all + restored a dropped assertion, deleted
a dead helper and the dead subscription_billed field) -> review.
* fix(db): post_update on AgentTable.current_task breaks the flush cycle
agents.current_task_id and tasks.assigned_to reference each other, so a
flush touching both rows — every claim now marks its agent ACTIVE — is
an instance-level circular dependency SQLAlchemy cannot topologically
sort. The e2e smoke's full verb paths (12 tests) hit it; the unit and
integration suites never flush both dirty rows with relationships
loaded. post_update emits the FK as a second UPDATE, the canonical fix
for mutually-referencing rows.
* fix(budgets): enforce only explicitly-set budgets — no per-TaskType defaults
The per-TaskType default cap table blocked an unbudgeted coordination
root one opus planning turn in ($1.50 PLANNING default vs. real
coordination spend) — a false positive by design the moment the fleet
runs a priced model. Budgets are now explicit-input only:
effective_task_budget_usd returns None for an unset budget_usd, the
budget sweep skips enforcement (and never prices spend) on None, and
the unblock re-check passes on None so clearing the budget field is
itself a valid resolution. The project monthly cap stays as the
explicit-input fleet-wide backstop. Panel copy tells the truth
("No cap" placeholder; empty = uncapped), and the TaskType default
table plus its resolver are deleted.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from roboco.api.deps import get_agent_context, get_db
|
||||
from roboco.api.routes.provider import router as provider_router
|
||||
from roboco.billing.pricing import input_price_per_million
|
||||
from roboco.db.tables import (
|
||||
ModelAssignmentTable,
|
||||
ProviderConfigTable,
|
||||
@@ -30,11 +31,19 @@ if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
def _first_model_for_type(provider_type: ModelProvider) -> str:
|
||||
def _unpriced_model_for_type(provider_type: ModelProvider) -> str:
|
||||
"""The first `provider_type` catalog entry pricing.py has NOT grounded a
|
||||
real per-token rate for — tests below want "an unpriced, free-tier
|
||||
downgrade-safe model" specifically to exercise provider-readiness gating,
|
||||
not pricing itself, so grounding a real rate for one catalog entry (e.g.
|
||||
GLM-5.2) must not silently break them by picking that one."""
|
||||
for entry in MODEL_CATALOG:
|
||||
if entry.provider_type == provider_type:
|
||||
if (
|
||||
entry.provider_type == provider_type
|
||||
and input_price_per_million(entry.model_name) == 0.0
|
||||
):
|
||||
return entry.model_name
|
||||
raise RuntimeError(f"no catalog entry for {provider_type}")
|
||||
raise RuntimeError(f"no unpriced catalog entry for {provider_type}")
|
||||
|
||||
|
||||
def _make_app(
|
||||
@@ -846,12 +855,12 @@ async def test_put_complexity_override_allows_same_tier_as_baseline(
|
||||
async def test_put_complexity_override_rejects_disabled_provider(
|
||||
app_client_with_ollama: AsyncClient,
|
||||
) -> None:
|
||||
"""qa's baseline (haiku) prices no cheaper than Ollama Cloud (unpriced,
|
||||
treated as free-tier) so the downgrade-only check passes — but the
|
||||
"""qa's baseline (haiku) prices no cheaper than an unpriced Ollama Cloud
|
||||
model (free-tier) so the downgrade-only check passes — but the
|
||||
OLLAMA_CLOUD provider is disabled (no key set) in this fixture's seeded
|
||||
state, so the write-time readiness guard rejects it before it can
|
||||
silently no-op to the legacy Anthropic path at spawn."""
|
||||
ollama_model = _first_model_for_type(ModelProvider.OLLAMA_CLOUD)
|
||||
ollama_model = _unpriced_model_for_type(ModelProvider.OLLAMA_CLOUD)
|
||||
response = await app_client_with_ollama.put(
|
||||
"/api/providers/complexity-overrides",
|
||||
json={"role": "qa", "complexity": "low", "model_name": ollama_model},
|
||||
@@ -875,7 +884,7 @@ async def test_put_complexity_override_warns_on_cross_family_once_provider_ready
|
||||
json={"api_key": "test-key"},
|
||||
headers=_HDR_PM,
|
||||
)
|
||||
ollama_model = _first_model_for_type(ModelProvider.OLLAMA_CLOUD)
|
||||
ollama_model = _unpriced_model_for_type(ModelProvider.OLLAMA_CLOUD)
|
||||
response = await app_client_with_ollama.put(
|
||||
"/api/providers/complexity-overrides",
|
||||
json={"role": "qa", "complexity": "low", "model_name": ollama_model},
|
||||
|
||||
Reference in New Issue
Block a user