refactor(providers): define typed usage contract

This commit is contained in:
chengyongru
2026-08-25 01:04:25 +08:00
committed by chengyongru
parent 89c94d8744
commit 9895c23cb5
84 changed files with 1643 additions and 726 deletions
+101 -30
View File
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest
from loguru import logger
from nanobot.providers.base import LLMUsage
from nanobot.providers.openai_responses.converters import (
convert_messages,
convert_tools,
@@ -484,7 +485,7 @@ class TestParseResponseOutput:
result = parse_response_output(resp)
assert result.content == "Hello!"
assert result.finish_reason == "stop"
assert result.usage == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
assert result.usage == LLMUsage.reported(input_tokens=10, output_tokens=5)
assert result.tool_calls == []
def test_refusal_response_surfaces_text_without_advancing_state(self):
@@ -652,7 +653,8 @@ class TestParseResponseOutput:
}
result = parse_response_output(mock)
assert result.content == "sdk"
assert result.usage["prompt_tokens"] == 1
assert result.usage is not None
assert result.usage.input_tokens == 1
def test_usage_maps_responses_api_keys(self):
"""Responses API uses input_tokens/output_tokens, not prompt_tokens/completion_tokens."""
@@ -662,9 +664,20 @@ class TestParseResponseOutput:
"usage": {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150},
}
result = parse_response_output(resp)
assert result.usage["prompt_tokens"] == 100
assert result.usage["completion_tokens"] == 50
assert result.usage["total_tokens"] == 150
assert result.usage == LLMUsage.reported(input_tokens=100, output_tokens=50)
def test_non_stream_preserves_provider_reported_total(self):
result = parse_response_output({
"output": [],
"status": "completed",
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 999},
})
assert result.usage == LLMUsage.reported(
input_tokens=10,
output_tokens=5,
total_tokens=999,
)
def test_preserves_every_output_item_as_opaque_state(self):
input_items = [{"role": "user", "content": "inspect the repo"}]
@@ -713,18 +726,18 @@ class TestResponsesConversationState:
{"type": "compaction", "encrypted_content": "compact"},
{"type": "message", "role": "assistant", "content": "new"},
],
usage={
"prompt_tokens": 90,
"completion_tokens": 10,
"total_tokens": 100,
},
usage=LLMUsage.reported(
input_tokens=90,
output_tokens=10,
total_tokens=175,
),
)
assert responses_state_items(state) == [
{"type": "compaction", "encrypted_content": "compact"},
{"type": "message", "role": "assistant", "content": "new"},
]
assert responses_state_context_tokens(state) == 100
assert responses_state_context_tokens(state) == 175
def test_existing_compaction_keeps_canonical_retained_prefix(self):
canonical_input = [
@@ -1090,7 +1103,7 @@ class TestConsumeSse:
assert content == "answer"
assert tool_calls == []
assert finish_reason == "stop"
assert usage == {}
assert usage is None
assert reasoning == "thinking briefly\nChecking result"
assert deltas == ["thinking ", "briefly", "\nChecking result"]
@@ -1224,7 +1237,7 @@ class TestConsumeSse:
assert content == "partial"
assert finish_reason == expected_finish_reason
assert usage == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
assert usage == LLMUsage.reported(input_tokens=10, output_tokens=5)
assert capture.completed is True
assert capture.response == terminal_response
assert capture.output_items == output
@@ -1296,7 +1309,10 @@ class TestConsumeSse:
"status": "completed",
"usage": {
"input_tokens": 10,
"input_tokens_details": {"cached_tokens": 8},
"input_tokens_details": {
"cached_tokens": 8,
"cache_write_tokens": 0,
},
"output_tokens": 5,
"total_tokens": 15,
},
@@ -1306,12 +1322,68 @@ class TestConsumeSse:
_, _, _, usage, _ = await consume_sse_with_reasoning(response)
assert usage == {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
"cached_tokens": 8,
assert usage == LLMUsage.reported(
input_tokens=10,
output_tokens=5,
cache_read_tokens=8,
cache_write_tokens=0,
)
@pytest.mark.asyncio
async def test_stream_and_non_stream_share_usage_normalization(self):
terminal = {
"status": "completed",
"output": [],
"usage": {
"input_tokens": 15,
"input_tokens_details": {
"cached_tokens": 0,
"cache_write_tokens": 7,
},
"output_tokens": 18,
"total_tokens": 175,
},
}
non_stream = parse_response_output(terminal).usage
sse = _SseResponse([
{"type": "response.completed", "response": terminal},
])
_, _, _, streamed, _ = await consume_sse_with_reasoning(sse)
sdk_response = SimpleNamespace(**terminal)
sdk_response.usage = SimpleNamespace(
input_tokens=15,
input_tokens_details=SimpleNamespace(
cached_tokens=0,
cache_write_tokens=7,
),
output_tokens=18,
total_tokens=175,
)
async def sdk_stream():
yield SimpleNamespace(type="response.completed", response=sdk_response)
_, _, _, sdk_streamed, _ = await consume_sdk_stream(sdk_stream())
expected = LLMUsage.reported(
input_tokens=15,
output_tokens=18,
total_tokens=175,
cache_read_tokens=0,
cache_write_tokens=7,
)
assert non_stream == streamed == sdk_streamed == expected
def test_missing_usage_is_not_explicit_zero_usage(self):
missing = parse_response_output({"status": "completed", "output": []})
explicit_zero = parse_response_output({
"status": "completed",
"output": [],
"usage": {"input_tokens": 0, "output_tokens": 0},
})
assert missing.usage is None
assert explicit_zero.usage == LLMUsage.reported(input_tokens=0, output_tokens=0)
@pytest.mark.asyncio
async def test_tool_call_done_arguments_callback(self):
@@ -1778,25 +1850,24 @@ class TestConsumeSdkStream:
@pytest.mark.asyncio
async def test_usage_extracted(self):
usage_obj = MagicMock(
usage_obj = SimpleNamespace(
input_tokens=10,
input_tokens_details=MagicMock(cached_tokens=8),
input_tokens_details=SimpleNamespace(cached_tokens=8),
output_tokens=5,
total_tokens=15,
)
resp_obj = MagicMock(status="completed", usage=usage_obj, output=[])
ev = MagicMock(type="response.completed", response=resp_obj)
resp_obj = SimpleNamespace(status="completed", usage=usage_obj, output=[])
ev = SimpleNamespace(type="response.completed", response=resp_obj)
async def stream():
yield ev
_, _, _, usage, _ = await consume_sdk_stream(stream())
assert usage == {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
"cached_tokens": 8,
}
assert usage == LLMUsage.reported(
input_tokens=10,
output_tokens=5,
cache_read_tokens=8,
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
@@ -1851,7 +1922,7 @@ class TestConsumeSdkStream:
assert content == "partial"
assert finish_reason == expected_finish_reason
assert usage == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
assert usage == LLMUsage.reported(input_tokens=10, output_tokens=5)
assert capture.completed is True
assert capture.response == terminal_response
assert capture.output_items == output