Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions src/agents/run_internal/model_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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


Expand Down
5 changes: 3 additions & 2 deletions src/agents/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 8 additions & 12 deletions tests/models/test_model_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 9 additions & 13 deletions tests/test_agent_runner_streamed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/test_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down