From 5b44ebdfd773274e01873f1f9f04026e98d31115 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Fri, 21 Aug 2026 13:41:15 +0800 Subject: [PATCH] fix(providers): report Responses cached tokens --- nanobot/providers/openai_responses/parsing.py | 14 +++++++++- tests/providers/test_openai_responses.py | 28 ++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/nanobot/providers/openai_responses/parsing.py b/nanobot/providers/openai_responses/parsing.py index 021569aa3..5ea213d89 100644 --- a/nanobot/providers/openai_responses/parsing.py +++ b/nanobot/providers/openai_responses/parsing.py @@ -203,11 +203,16 @@ def _usage_from_response_obj(response: object) -> dict[str, int]: usage.get("output_tokens") or usage.get("completion_tokens") or 0 ) total_tokens = int(usage.get("total_tokens") or prompt_tokens + completion_tokens) - return { + result = { "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": total_tokens, } + input_details = _response_object(usage.get("input_tokens_details")) + cached_tokens = int(input_details.get("cached_tokens") or 0) if input_details else 0 + if cached_tokens > 0: + result["cached_tokens"] = cached_tokens + return result def _parse_tool_call_arguments(args_raw: Any, name: str | None) -> Any: @@ -789,6 +794,13 @@ async def consume_sdk_stream( "completion_tokens": int(getattr(usage_obj, "output_tokens", 0) or 0), "total_tokens": int(getattr(usage_obj, "total_tokens", 0) or 0), } + usage_data = _response_object(usage_obj) or {} + input_details = _response_object(usage_data.get("input_tokens_details")) + cached_tokens = ( + int(input_details.get("cached_tokens") or 0) if input_details else 0 + ) + if cached_tokens > 0: + usage["cached_tokens"] = cached_tokens if not reasoning_content: reasoning_content = _extract_reasoning_summary_from_output( getattr(resp, "output", None) diff --git a/tests/providers/test_openai_responses.py b/tests/providers/test_openai_responses.py index af02add99..68747094f 100644 --- a/tests/providers/test_openai_responses.py +++ b/tests/providers/test_openai_responses.py @@ -1278,14 +1278,24 @@ class TestConsumeSse: "type": "response.completed", "response": { "status": "completed", - "usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}, + "usage": { + "input_tokens": 10, + "input_tokens_details": {"cached_tokens": 8}, + "output_tokens": 5, + "total_tokens": 15, + }, }, }, ]) _, _, _, usage, _ = await consume_sse_with_reasoning(response) - assert usage == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15} + assert usage == { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cached_tokens": 8, + } @pytest.mark.asyncio async def test_tool_call_done_arguments_callback(self): @@ -1752,7 +1762,12 @@ class TestConsumeSdkStream: @pytest.mark.asyncio async def test_usage_extracted(self): - usage_obj = MagicMock(input_tokens=10, output_tokens=5, total_tokens=15) + usage_obj = MagicMock( + input_tokens=10, + input_tokens_details=MagicMock(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) @@ -1760,7 +1775,12 @@ class TestConsumeSdkStream: yield ev _, _, _, usage, _ = await consume_sdk_stream(stream()) - assert usage == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15} + assert usage == { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cached_tokens": 8, + } @pytest.mark.asyncio @pytest.mark.parametrize(