fix(streaming): keep provider usage-only chunks for cost tracking without include_usage

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
milan 2026-07-28 23:05:55 +00:00
parent 7cd009caf7
commit b2d2b29e2f
2 changed files with 80 additions and 0 deletions

View file

@ -1465,6 +1465,7 @@ class CustomStreamWrapper:
if self.stream_options is not None and self.stream_options["include_usage"] is True:
model_response.choices = []
return model_response
self._record_usage_only_chunk(model_response=model_response)
return
## CHECK FOR TOOL USE
@ -1691,6 +1692,17 @@ class CustomStreamWrapper:
model_response.choices[0].finish_reason = "tool_calls"
return model_response
def _record_usage_only_chunk(self, model_response: "ModelResponseStream") -> None:
"""
Keep provider usage-only chunks (e.g. OpenRouter's post-finish chunk, which carries a
provider-reported cost) available to cost tracking. They are never returned to the
caller; ``stream_options.include_usage`` only controls what the caller sees.
"""
if getattr(model_response, "usage", None) is None:
return
model_response.choices = []
self.chunks.append(model_response)
@staticmethod
def _propagate_usage_cost_to_hidden_params(
response: "ModelResponse",

View file

@ -1507,6 +1507,74 @@ async def test_openrouter_streaming_cost_after_finish_reason(logging_obj: Loggin
assert usage_chunks[-1].usage.cost == 0.00025
@pytest.mark.asyncio
async def test_openrouter_streaming_usage_only_chunk_without_stream_options(
logging_obj: Logging,
):
"""
Regression: OpenRouter's post-finish chunk has `choices: []`. When the caller did not
pass stream_options.include_usage it was dropped before cost tracking, so the
provider-reported cost never reached the assembled response.
"""
from litellm.cost_calculator import get_response_cost_from_hidden_params
from litellm.utils import ModelResponseListIterator
chunk1 = ModelResponseStream(
id="chatcmpl-or",
created=1742056047,
model="openrouter/claude",
choices=[
StreamingChoices(
finish_reason=None, index=0, delta=Delta(content="Hi", role="assistant")
)
],
usage=None,
)
chunk2 = ModelResponseStream(
id="chatcmpl-or",
created=1742056048,
model="openrouter/claude",
choices=[
StreamingChoices(finish_reason="stop", index=0, delta=Delta(content=""))
],
usage=None,
)
usage_only_chunk = ModelResponseStream(
id="chatcmpl-or",
created=1742056049,
model="openrouter/claude",
choices=[],
usage=Usage(
completion_tokens=5, prompt_tokens=10, total_tokens=15, cost=0.00025
),
)
response = CustomStreamWrapper(
completion_stream=ModelResponseListIterator(
model_responses=[chunk1, chunk2, usage_only_chunk]
),
model="openrouter/claude",
custom_llm_provider="openrouter",
logging_obj=logging_obj,
)
collected_chunks = [chunk async for chunk in response]
assert all(getattr(chunk, "usage", None) is None for chunk in collected_chunks)
complete_response = litellm.stream_chunk_builder(
chunks=response.chunks,
messages=[{"role": "user", "content": "Hey"}],
)
assert complete_response is not None
assert complete_response.usage.cost == 0.00025
CustomStreamWrapper._propagate_usage_cost_to_hidden_params(complete_response)
assert (
get_response_cost_from_hidden_params(complete_response._hidden_params) == 0.00025
)
def test_openrouter_streaming_cost_propagates_to_hidden_params():
"""
Verify that provider-reported cost from usage.cost flows into