refactor(agent): make run usage explicit (#5546)

* refactor(agent): make run usage explicit

* fix(api): capture usage per run
This commit is contained in:
chengyongru
2026-08-26 15:18:53 +08:00
committed by GitHub
parent 0c84725b13
commit 4f6c0aedfa
20 changed files with 184 additions and 233 deletions
+25 -12
View File
@@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
import pytest_asyncio
from nanobot.agent.hook import AgentHook, AgentRunHookContext
from nanobot.api.server import (
API_CHAT_ID,
API_SESSION_KEY,
@@ -36,7 +37,6 @@ def _make_mock_agent(response_text: str = "mock response") -> MagicMock:
agent = MagicMock()
agent.process_direct = AsyncMock(return_value=response_text)
agent.aclose = AsyncMock()
agent._last_usage = LLMUsage.reported(input_tokens=100, output_tokens=50)
return agent
@@ -296,7 +296,18 @@ async def test_single_user_message_must_have_user_role() -> None:
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
@pytest.mark.asyncio
async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_agent) -> None:
async def test_successful_request_uses_fixed_api_session_and_run_usage(
aiohttp_client,
mock_agent,
) -> None:
usage = LLMUsage.reported(input_tokens=100, output_tokens=50)
async def process_direct(*, hooks: list[AgentHook], **_kwargs: object) -> str:
for hook in hooks:
await hook.after_run(AgentRunHookContext(messages=[], usage=usage))
return "mock response"
mock_agent.process_direct = AsyncMock(side_effect=process_direct)
app = create_app(mock_agent, model_name="test-model", api_key=API_KEY)
client = await aiohttp_client(app)
resp = await client.post(
@@ -308,13 +319,18 @@ async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_ag
body = await resp.json()
assert body["choices"][0]["message"]["content"] == "mock response"
assert body["model"] == "test-model"
mock_agent.process_direct.assert_called_once_with(
content="hello",
media=None,
session_key=API_SESSION_KEY,
channel="api",
chat_id=API_CHAT_ID,
)
assert body["usage"] == {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150,
}
call_kwargs = mock_agent.process_direct.call_args.kwargs
assert call_kwargs["content"] == "hello"
assert call_kwargs["media"] is None
assert call_kwargs["session_key"] == API_SESSION_KEY
assert call_kwargs["channel"] == "api"
assert call_kwargs["chat_id"] == API_CHAT_ID
assert len(call_kwargs["hooks"]) == 1
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
@@ -329,7 +345,6 @@ async def test_followup_requests_share_same_session_key(aiohttp_client) -> None:
agent = MagicMock()
agent.process_direct = fake_process
agent.aclose = AsyncMock()
agent._last_usage = None
app = create_app(agent, model_name="m", api_key=API_KEY)
client = await aiohttp_client(app)
@@ -368,7 +383,6 @@ async def test_fixed_session_requests_are_serialized(aiohttp_client) -> None:
agent = MagicMock()
agent.process_direct = slow_process
agent.aclose = AsyncMock()
agent._last_usage = None
app = create_app(agent, model_name="m", api_key=API_KEY)
client = await aiohttp_client(app)
@@ -485,7 +499,6 @@ async def test_empty_response_falls_back_without_retry(aiohttp_client) -> None:
agent = MagicMock()
agent.process_direct = always_empty
agent.aclose = AsyncMock()
agent._last_usage = None
app = create_app(agent, model_name="m", api_key=API_KEY)
client = await aiohttp_client(app)