mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(openrouter): use provider-reported usage in streaming without stream_options
When providers like OpenRouter send a usage chunk after the finish_reason chunk, _hidden_params["usage"] was already calculated (with zeros) before the usage data arrived. The StopIteration handler now recalculates usage from stream_chunk_builder and updates the shared _hidden_params dict so the user's copy reflects the real provider-reported token counts. Fixes #20760
This commit is contained in:
parent
bac1b6b2e0
commit
27413790e6
2 changed files with 123 additions and 0 deletions
|
|
@ -149,6 +149,7 @@ class CustomStreamWrapper:
|
|||
) # keep track of the returned chunks - used for calculating the input/output tokens for stream options
|
||||
self.is_function_call = self.check_is_function_call(logging_obj=logging_obj)
|
||||
self.created: Optional[int] = None
|
||||
self._last_returned_hidden_params: Optional[dict] = None
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
|
@ -1787,6 +1788,7 @@ class CustomStreamWrapper:
|
|||
if self.sent_last_chunk is True and self.stream_options is None:
|
||||
usage = calculate_total_usage(chunks=self.chunks)
|
||||
response._hidden_params["usage"] = usage
|
||||
self._last_returned_hidden_params = response._hidden_params
|
||||
# Add MCP metadata to final chunk if present
|
||||
response = self._add_mcp_metadata_to_final_chunk(response)
|
||||
# RETURN RESULT
|
||||
|
|
@ -1828,6 +1830,24 @@ class CustomStreamWrapper:
|
|||
None,
|
||||
cache_hit,
|
||||
)
|
||||
# Update hidden_params with final usage from
|
||||
# stream_chunk_builder. Some providers (e.g. OpenRouter)
|
||||
# send usage in a chunk after finish_reason, which arrives
|
||||
# after _hidden_params["usage"] was initially set. The
|
||||
# _hidden_params dict is the same object the user received
|
||||
# (shared by reference), so mutating it here also corrects
|
||||
# the user's copy.
|
||||
if (
|
||||
self.stream_options is None
|
||||
and complete_streaming_response is not None
|
||||
and self._last_returned_hidden_params is not None
|
||||
):
|
||||
final_usage = getattr(
|
||||
complete_streaming_response, "usage", None
|
||||
)
|
||||
if final_usage is not None:
|
||||
self._last_returned_hidden_params["usage"] = final_usage
|
||||
|
||||
if self.sent_stream_usage is False and self.send_stream_usage is True:
|
||||
self.sent_stream_usage = True
|
||||
return response
|
||||
|
|
@ -1951,6 +1971,7 @@ class CustomStreamWrapper:
|
|||
if self.sent_last_chunk is True and self.stream_options is None:
|
||||
usage = calculate_total_usage(chunks=self.chunks)
|
||||
processed_chunk._hidden_params["usage"] = usage
|
||||
self._last_returned_hidden_params = processed_chunk._hidden_params
|
||||
|
||||
# Call post-call streaming deployment hook for final chunk
|
||||
if self.sent_last_chunk is True:
|
||||
|
|
@ -2017,6 +2038,19 @@ class CustomStreamWrapper:
|
|||
cache_hit=cache_hit,
|
||||
)
|
||||
)
|
||||
# Update hidden_params with final usage from
|
||||
# stream_chunk_builder (see sync __next__ for full comment).
|
||||
if (
|
||||
self.stream_options is None
|
||||
and complete_streaming_response is not None
|
||||
and self._last_returned_hidden_params is not None
|
||||
):
|
||||
final_usage = getattr(
|
||||
complete_streaming_response, "usage", None
|
||||
)
|
||||
if final_usage is not None:
|
||||
self._last_returned_hidden_params["usage"] = final_usage
|
||||
|
||||
if self.sent_stream_usage is False and self.send_stream_usage is True:
|
||||
self.sent_stream_usage = True
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -1185,3 +1185,92 @@ def test_is_chunk_non_empty_with_valid_tool_calls(
|
|||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_usage_chunk_after_finish_reason_updates_hidden_params(logging_obj):
|
||||
"""
|
||||
Test that provider-reported usage from a post-finish_reason chunk
|
||||
is surfaced in _hidden_params even when stream_options is NOT set.
|
||||
|
||||
Reproduces issue #20760: OpenRouter sends a final chunk with usage data
|
||||
after the finish_reason chunk. The hidden_params["usage"] on the last
|
||||
user-visible chunk was being calculated before this usage chunk arrived,
|
||||
resulting in zeros. The fix recalculates it in the StopIteration handler
|
||||
after stream_chunk_builder processes all chunks.
|
||||
"""
|
||||
# Simulate OpenRouter's actual streaming pattern:
|
||||
# 1) content chunk
|
||||
# 2) finish_reason chunk (content="")
|
||||
# 3) usage chunk (content="", finish_reason=None, usage={...})
|
||||
chunks = [
|
||||
ModelResponseStream(
|
||||
id="gen-abc",
|
||||
object="chat.completion.chunk",
|
||||
created=1000000,
|
||||
model="openrouter/openai/gpt-4o-mini",
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(role="assistant", content="Hello"),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
),
|
||||
ModelResponseStream(
|
||||
id="gen-abc",
|
||||
object="chat.completion.chunk",
|
||||
created=1000000,
|
||||
model="openrouter/openai/gpt-4o-mini",
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(content=""),
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
),
|
||||
ModelResponseStream(
|
||||
id="gen-abc",
|
||||
object="chat.completion.chunk",
|
||||
created=1000000,
|
||||
model="openrouter/openai/gpt-4o-mini",
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(role="assistant", content=""),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
usage=Usage(
|
||||
prompt_tokens=20,
|
||||
completion_tokens=135,
|
||||
total_tokens=155,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
# Create a CustomStreamWrapper with NO stream_options
|
||||
wrapper = CustomStreamWrapper(
|
||||
completion_stream=ModelResponseListIterator(model_responses=chunks),
|
||||
model="openrouter/openai/gpt-4o-mini",
|
||||
logging_obj=logging_obj,
|
||||
custom_llm_provider="openrouter",
|
||||
stream_options=None,
|
||||
)
|
||||
|
||||
# Consume the stream
|
||||
collected = []
|
||||
for chunk in wrapper:
|
||||
collected.append(chunk)
|
||||
|
||||
# The last user-visible chunk's _hidden_params["usage"] should
|
||||
# contain the provider-reported values, not zeros.
|
||||
last_chunk = collected[-1]
|
||||
hidden_usage = last_chunk._hidden_params.get("usage")
|
||||
assert hidden_usage is not None, "Expected usage in _hidden_params"
|
||||
assert hidden_usage.prompt_tokens == 20, (
|
||||
f"Expected prompt_tokens=20 from provider, got {hidden_usage.prompt_tokens}"
|
||||
)
|
||||
assert hidden_usage.completion_tokens == 135, (
|
||||
f"Expected completion_tokens=135 from provider, got {hidden_usage.completion_tokens}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue