Question
request_usage_entries is documented as being "for accurate per-request cost
calculation". Two code paths populate it, and they disagree about what to do
with a request whose token counts are all zero. I could not tell from the source
which behaviour is intended, so this is a question about the contract rather
than a bug report.
Measured
On 45e02bd, four cases. The elif in Usage.add() that decides this is
byte-identical on main, so the result carries over.
A) add() with a zero-token request requests=1 entries=0
B) add() with a normal request requests=1 entries=1
C) one success after two failed retries requests=3 entries=3 entry tokens [0, 0, 15]
D) folding C into a run total requests=3 entries=3 sum 15 = total_tokens 15
from agents.usage import Usage
from agents.run_internal.model_retry import apply_retry_attempt_usage
run = Usage()
run.add(Usage(requests=1, input_tokens=0, output_tokens=0, total_tokens=0))
print(run.requests, len(run.request_usage_entries)) # 1 0
u = Usage(requests=1, input_tokens=10, output_tokens=5, total_tokens=15)
apply_retry_attempt_usage(u, failed_attempts=2)
print(u.requests, [e.total_tokens for e in u.request_usage_entries]) # 3 [0, 0, 15]
Why the two disagree
Usage.add() skips the entry unless there are tokens:
elif other.requests == 1 and other.total_tokens > 0:
apply_retry_attempt_usage() creates one per failed attempt, with zeros:
usage.request_usage_entries = [
_build_zero_request_usage_entry() for _ in range(failed_attempts)
] + successful_request_entries
So a request the SDK knows nothing about is represented two opposite ways
depending on which path produced it: invisible in one, an explicit zero in the
other. A consumer reading the list cannot distinguish "this request cost
nothing" from "this request is not in the list", and both are silent.
Why case A is not exotic
Usage(requests=N) with all-zero tokens is not a malformed value, it is what the
SDK deliberately produces when a provider reports no usage:
# chatcmpl_stream_handler.py
if usage is None:
# The stream reached a terminal response, so a request was made even though the
# provider reported no usage. Record that without inventing a usage payload, so
# the raw usage snapshot stays absent and tokens are not reported as real zeros.
_mark_request_completed_without_usage(final_response)
Any Chat Completions stream that ends without a usage chunk takes this path, and
any_llm_model.py mirrors it. That is the default for providers that do not send
stream_options={"include_usage": true}, so it is a common configuration rather
than a provider defect. any_llm_model.py:529 and litellm_model.py:488 then
build Usage(requests=...) from it with the token fields left at zero, which is
exactly case A.
The comment above is also the part that makes the retry behaviour look
inconsistent rather than merely different: it states the intent as tokens must
not be reported as real zeros, and apply_retry_attempt_usage reports real
zeros.
How this differs from #3696
#3696 proposed keying the entry off any non-zero token count, and was held
pending a real provider trace where total_tokens is omitted while
input_tokens is not. That is a different case and I am not reopening it. Here
every count is genuinely zero because there is no usage payload at all, so that
fix would not create an entry for case A either, and the trigger is an ordinary
streaming configuration rather than a provider omitting a field it should send.
The question
Which is the intended contract for request_usage_entries?
- One entry per request, so
len(entries) == requests always holds and unknown
usage appears as a zero entry. This makes add() the odd one out.
- Entries only for requests with known usage, so a consumer must treat the list
as a subset and read requests for the count. This makes
apply_retry_attempt_usage the odd one out.
RequestUsage has non-optional int fields, so it cannot currently express
"counted, usage unknown", which may be why the two paths diverged. Happy to send
a PR for whichever answer you prefer, including the RequestUsage change if the
third option is the one you want.
Question
request_usage_entriesis documented as being "for accurate per-request costcalculation". Two code paths populate it, and they disagree about what to do
with a request whose token counts are all zero. I could not tell from the source
which behaviour is intended, so this is a question about the contract rather
than a bug report.
Measured
On
45e02bd, four cases. TheelifinUsage.add()that decides this isbyte-identical on
main, so the result carries over.Why the two disagree
Usage.add()skips the entry unless there are tokens:apply_retry_attempt_usage()creates one per failed attempt, with zeros:So a request the SDK knows nothing about is represented two opposite ways
depending on which path produced it: invisible in one, an explicit zero in the
other. A consumer reading the list cannot distinguish "this request cost
nothing" from "this request is not in the list", and both are silent.
Why case A is not exotic
Usage(requests=N)with all-zero tokens is not a malformed value, it is what theSDK deliberately produces when a provider reports no usage:
Any Chat Completions stream that ends without a usage chunk takes this path, and
any_llm_model.pymirrors it. That is the default for providers that do not sendstream_options={"include_usage": true}, so it is a common configuration ratherthan a provider defect.
any_llm_model.py:529andlitellm_model.py:488thenbuild
Usage(requests=...)from it with the token fields left at zero, which isexactly case A.
The comment above is also the part that makes the retry behaviour look
inconsistent rather than merely different: it states the intent as tokens must
not be reported as real zeros, and
apply_retry_attempt_usagereports realzeros.
How this differs from #3696
#3696 proposed keying the entry off any non-zero token count, and was held
pending a real provider trace where
total_tokensis omitted whileinput_tokensis not. That is a different case and I am not reopening it. Hereevery count is genuinely zero because there is no usage payload at all, so that
fix would not create an entry for case A either, and the trigger is an ordinary
streaming configuration rather than a provider omitting a field it should send.
The question
Which is the intended contract for
request_usage_entries?len(entries) == requestsalways holds and unknownusage appears as a zero entry. This makes
add()the odd one out.as a subset and read
requestsfor the count. This makesapply_retry_attempt_usagethe odd one out.RequestUsagehas non-optionalintfields, so it cannot currently express"counted, usage unknown", which may be why the two paths diverged. Happy to send
a PR for whichever answer you prefer, including the
RequestUsagechange if thethird option is the one you want.