diff --git a/src/agents/run_internal/model_retry.py b/src/agents/run_internal/model_retry.py index 994e1c0e32..93e14f3cf8 100644 --- a/src/agents/run_internal/model_retry.py +++ b/src/agents/run_internal/model_retry.py @@ -315,16 +315,6 @@ async def _await_model_attempt( raise timeout_error from None -def _build_zero_request_usage_entry() -> RequestUsage: - return RequestUsage( - input_tokens=0, - output_tokens=0, - total_tokens=0, - input_tokens_details=Usage().input_tokens_details, - output_tokens_details=Usage().output_tokens_details, - ) - - def _build_request_usage_entry_from_usage(usage: Usage) -> RequestUsage: return RequestUsage( input_tokens=usage.input_tokens, @@ -339,14 +329,14 @@ def apply_retry_attempt_usage(usage: Usage, failed_attempts: int) -> Usage: if failed_attempts <= 0: return usage - successful_request_entries = list(usage.request_usage_entries) - if not successful_request_entries: - successful_request_entries.append(_build_request_usage_entry_from_usage(usage)) + request_entries = list(usage.request_usage_entries) + if not request_entries and usage.total_tokens > 0: + request_entries.append(_build_request_usage_entry_from_usage(usage)) usage.requests = max(usage.requests, 1) + failed_attempts - usage.request_usage_entries = [ - _build_zero_request_usage_entry() for _ in range(failed_attempts) - ] + successful_request_entries + # Failed attempts do not carry provider usage. Keep them in the aggregate request count, + # but do not synthesize zero-valued entries that look like known per-request usage. + usage.request_usage_entries = request_entries return usage diff --git a/src/agents/usage.py b/src/agents/usage.py index 81adfff27c..24092340e3 100644 --- a/src/agents/usage.py +++ b/src/agents/usage.py @@ -218,8 +218,9 @@ class Usage: request_usage_entries: list[RequestUsage] = field(default_factory=list) """List of RequestUsage entries for accurate per-request cost calculation. - Each call to `add()` automatically creates an entry in this list if the added usage - represents a new request (i.e., has non-zero tokens). + Entries are created only when token usage is known for an individual request. A request whose + provider reports no usage still increments `requests`, but it does not create a synthetic + zero-valued entry. As a result, this list can contain fewer entries than `requests`. Example: For a run that makes 3 API calls with 100K, 150K, and 80K input tokens each, diff --git a/tests/models/test_model_retry.py b/tests/models/test_model_retry.py index c8079ab8a7..c200fad320 100644 --- a/tests/models/test_model_retry.py +++ b/tests/models/test_model_retry.py @@ -921,15 +921,14 @@ async def get_response() -> ModelResponse: ) assert result.usage.requests == 2 - assert len(result.usage.request_usage_entries) == 2 - assert result.usage.request_usage_entries[0].total_tokens == 0 - assert result.usage.request_usage_entries[1].input_tokens == 11 - assert result.usage.request_usage_entries[1].output_tokens == 7 - assert result.usage.request_usage_entries[1].total_tokens == 18 + assert len(result.usage.request_usage_entries) == 1 + assert result.usage.request_usage_entries[0].input_tokens == 11 + assert result.usage.request_usage_entries[0].output_tokens == 7 + assert result.usage.request_usage_entries[0].total_tokens == 18 @pytest.mark.asyncio -async def test_get_response_with_retry_preserves_zero_token_successful_request_usage_entry( +async def test_get_response_with_retry_omits_unknown_successful_request_usage_entry( monkeypatch, ) -> None: calls = 0 @@ -967,9 +966,7 @@ async def get_response() -> ModelResponse: ) assert result.usage.requests == 2 - assert len(result.usage.request_usage_entries) == 2 - assert result.usage.request_usage_entries[0].total_tokens == 0 - assert result.usage.request_usage_entries[1].total_tokens == 0 + assert result.usage.request_usage_entries == [] @pytest.mark.asyncio @@ -1126,9 +1123,8 @@ async def get_response() -> ModelResponse: assert rewinds == 1 assert sleeps == [1.0] assert result.usage.requests == 2 - assert len(result.usage.request_usage_entries) == 2 - assert result.usage.request_usage_entries[0].total_tokens == 0 - assert result.usage.request_usage_entries[1].total_tokens == 5 + assert len(result.usage.request_usage_entries) == 1 + assert result.usage.request_usage_entries[0].total_tokens == 5 @pytest.mark.asyncio diff --git a/tests/test_agent_runner_streamed.py b/tests/test_agent_runner_streamed.py index e5c2a03fd8..c2f335824e 100644 --- a/tests/test_agent_runner_streamed.py +++ b/tests/test_agent_runner_streamed.py @@ -431,11 +431,10 @@ async def test_streamed_run_preserves_request_usage_entries_after_retry() -> Non usage = result.context_wrapper.usage assert usage.requests == 2 - assert len(usage.request_usage_entries) == 2 - assert usage.request_usage_entries[0].total_tokens == 0 - assert usage.request_usage_entries[1].input_tokens == 10 - assert usage.request_usage_entries[1].output_tokens == 5 - assert usage.request_usage_entries[1].total_tokens == 15 + assert len(usage.request_usage_entries) == 1 + assert usage.request_usage_entries[0].input_tokens == 10 + assert usage.request_usage_entries[0].output_tokens == 5 + assert usage.request_usage_entries[0].total_tokens == 15 @pytest.mark.asyncio @@ -483,9 +482,7 @@ async def missing_usage_stream(_call) -> AsyncIterator[TResponseStreamEvent]: usage = result.context_wrapper.usage assert len(model.calls) == 2 assert usage.requests == 2 - assert len(usage.request_usage_entries) == 2 - assert usage.request_usage_entries[0].total_tokens == 0 - assert usage.request_usage_entries[1].total_tokens == 0 + assert usage.request_usage_entries == [] @pytest.mark.asyncio @@ -557,11 +554,10 @@ async def test_streamed_run_preserves_request_usage_entries_after_conversation_l usage = result.context_wrapper.usage assert usage.requests == 2 - assert len(usage.request_usage_entries) == 2 - assert usage.request_usage_entries[0].total_tokens == 0 - assert usage.request_usage_entries[1].input_tokens == 10 - assert usage.request_usage_entries[1].output_tokens == 5 - assert usage.request_usage_entries[1].total_tokens == 15 + assert len(usage.request_usage_entries) == 1 + assert usage.request_usage_entries[0].input_tokens == 10 + assert usage.request_usage_entries[0].output_tokens == 5 + assert usage.request_usage_entries[0].total_tokens == 15 @pytest.mark.allow_call_model_methods diff --git a/tests/test_usage.py b/tests/test_usage.py index 5eef65543c..a756298a83 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -223,7 +223,8 @@ def test_usage_add_ignores_zero_token_requests(): u1.add(u2) - # Should not create a request_usage_entry for zero tokens + assert u1.requests == 1 + # The aggregate request count remains authoritative when token usage is unavailable. assert len(u1.request_usage_entries) == 0