mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
"""_sweep_token_snapshots prices cache tokens, not just input/output.
|
|||
|
|
|
||
|
|
The live USAGE_SNAPSHOT cost must match calculate_cost over the full 4-tuple
|
||
|
|
(the finalize path already does); dropping cache read/write undercounts
|
||
|
|
Anthropic cache spend mid-run.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import roboco.db.base as db_base
|
||
|
|
from roboco.billing import pricing
|
||
|
|
from roboco.models.runtime import AgentInstance
|
||
|
|
from roboco.runtime.orchestrator import AgentOrchestrator, AgentState
|
||
|
|
|
||
|
|
# 1M input, 0 output, 4M cache read, 0 cache write — cache spend dominates.
|
||
|
|
TOKENS_INPUT = 1_000_000
|
||
|
|
TOKENS_CACHE_READ = 4_000_000
|
||
|
|
|
||
|
|
|
||
|
|
def _active_orch() -> tuple[AgentOrchestrator, AgentInstance]:
|
||
|
|
orch = AgentOrchestrator.__new__(AgentOrchestrator)
|
||
|
|
cfg = type("C", (), {"provider_type": "anthropic", "model": "claude-sonnet-5"})()
|
||
|
|
inst = AgentInstance(agent_id="be-dev-1", state=AgentState.ACTIVE, config=cfg)
|
||
|
|
inst.container_id = "cid"
|
||
|
|
orch._instances = {"be-dev-1": inst}
|
||
|
|
return orch, inst
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_sweep_passes_cache_tokens_to_calculate_cost(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
orch, _inst = _active_orch()
|
||
|
|
captured: dict[str, Any] = {}
|
||
|
|
|
||
|
|
def fake_calc(
|
||
|
|
model: str,
|
||
|
|
tokens_input: int,
|
||
|
|
tokens_output: int,
|
||
|
|
tokens_cache_read: int = 0,
|
||
|
|
tokens_cache_write: int = 0,
|
||
|
|
) -> float:
|
||
|
|
captured.update(
|
||
|
|
model=model,
|
||
|
|
tokens_input=tokens_input,
|
||
|
|
tokens_output=tokens_output,
|
||
|
|
tokens_cache_read=tokens_cache_read,
|
||
|
|
tokens_cache_write=tokens_cache_write,
|
||
|
|
)
|
||
|
|
return 0.804 # 4M * 0.201/1M cache-read spend
|
||
|
|
|
||
|
|
monkeypatch.setattr(pricing, "calculate_cost", fake_calc)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
orch,
|
||
|
|
"_resolve_active_tokens",
|
||
|
|
AsyncMock(return_value=(TOKENS_INPUT, 0, TOKENS_CACHE_READ, 0)),
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
orch,
|
||
|
|
"_persist_token_snapshot",
|
||
|
|
AsyncMock(return_value=True),
|
||
|
|
)
|
||
|
|
# Stub the session factory import path the sweep uses.
|
||
|
|
fake_session = AsyncMock()
|
||
|
|
fake_session.__aenter__ = AsyncMock(return_value=fake_session)
|
||
|
|
fake_session.__aexit__ = AsyncMock(return_value=None)
|
||
|
|
factory = MagicMock(return_value=fake_session)
|
||
|
|
monkeypatch.setattr(db_base, "get_session_factory", lambda: factory)
|
||
|
|
# No publish target needed; the post-loop publish is best-effort.
|
||
|
|
monkeypatch.setattr(orch, "_publish_usage_snapshot", AsyncMock(), raising=False)
|
||
|
|
|
||
|
|
await orch._sweep_token_snapshots()
|
||
|
|
|
||
|
|
assert captured["tokens_cache_read"] == TOKENS_CACHE_READ
|
||
|
|
assert captured["tokens_cache_write"] == 0
|
||
|
|
assert captured["tokens_input"] == TOKENS_INPUT
|