mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-23 05:34:41 +00:00
fix(completion): record missing provider usage on the response's hidden params
This commit is contained in:
parent
4e2dd65549
commit
a96543658b
3 changed files with 146 additions and 0 deletions
|
|
@ -800,6 +800,15 @@ def convert_to_model_response_object(
|
|||
if "usage" in response_object and response_object["usage"] is not None:
|
||||
usage_object: Final = litellm.Usage(**response_object["usage"])
|
||||
setattr(model_response_object, "usage", usage_object)
|
||||
else:
|
||||
# The provider sent no usage (missing or explicit null). The
|
||||
# pre-attached empty Usage() keeps zero-filled counts as the
|
||||
# default for downstream consumers, but a missing block would
|
||||
# be indistinguishable from a real zero - record the absence
|
||||
# so callers can tell the two apart.
|
||||
cast( # cast-ok: hidden_params is normalized to a dict in the function prelude
|
||||
dict[str, object], hidden_params
|
||||
).update({"usage_missing": True})
|
||||
if "created" in response_object:
|
||||
model_response_object.created = safe_convert_created_field(response_object["created"])
|
||||
|
||||
|
|
|
|||
|
|
@ -2968,3 +2968,73 @@ def test_convert_to_model_response_object_function_output():
|
|||
assert result.usage.completion_tokens_details == CompletionTokensDetailsWrapper(reasoning_tokens=0)
|
||||
|
||||
assert result._hidden_params is not None
|
||||
|
||||
|
||||
COMPLETION_RESPONSE_WITHOUT_USAGE = {
|
||||
"id": "chatcmpl-no-usage",
|
||||
"created": 1784657740,
|
||||
"model": "gpt-5.6",
|
||||
"object": "chat.completion",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "Hello"},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
COMPLETION_RESPONSE_NULL_USAGE = {**COMPLETION_RESPONSE_WITHOUT_USAGE, "usage": None}
|
||||
|
||||
COMPLETION_RESPONSE_ZERO_USAGE = {
|
||||
**COMPLETION_RESPONSE_WITHOUT_USAGE,
|
||||
"usage": {"completion_tokens": 0, "prompt_tokens": 0, "total_tokens": 0},
|
||||
}
|
||||
|
||||
|
||||
def test_convert_to_model_response_object_marks_missing_usage():
|
||||
# main.py's completion() pre-attaches an empty Usage() before the provider
|
||||
# reply is parsed; without a marker, a missing block looks like a real 0.
|
||||
model_response: Final = ModelResponse()
|
||||
setattr(model_response, "usage", litellm.Usage())
|
||||
|
||||
result: Final = convert_to_model_response_object(
|
||||
response_object=COMPLETION_RESPONSE_WITHOUT_USAGE,
|
||||
model_response_object=model_response,
|
||||
response_type="completion",
|
||||
)
|
||||
|
||||
assert result.hidden_params["usage_missing"] is True
|
||||
# the zero-filled default stays intact for downstream consumers
|
||||
assert result.usage.prompt_tokens == 0
|
||||
assert result.usage.completion_tokens == 0
|
||||
|
||||
|
||||
def test_convert_to_model_response_object_marks_null_usage():
|
||||
model_response: Final = ModelResponse()
|
||||
setattr(model_response, "usage", litellm.Usage())
|
||||
|
||||
result: Final = convert_to_model_response_object(
|
||||
response_object=COMPLETION_RESPONSE_NULL_USAGE,
|
||||
model_response_object=model_response,
|
||||
response_type="completion",
|
||||
)
|
||||
|
||||
assert result.hidden_params["usage_missing"] is True
|
||||
assert result.usage.total_tokens == 0
|
||||
|
||||
|
||||
def test_convert_to_model_response_object_real_zero_usage_not_marked():
|
||||
model_response: Final = ModelResponse()
|
||||
setattr(model_response, "usage", litellm.Usage())
|
||||
|
||||
result: Final = convert_to_model_response_object(
|
||||
response_object=COMPLETION_RESPONSE_ZERO_USAGE,
|
||||
model_response_object=model_response,
|
||||
response_type="completion",
|
||||
)
|
||||
|
||||
assert "usage_missing" not in result.hidden_params
|
||||
assert result.usage.prompt_tokens == 0
|
||||
assert result.usage.completion_tokens == 0
|
||||
assert result.usage.total_tokens == 0
|
||||
|
|
|
|||
|
|
@ -8732,3 +8732,70 @@ async def test_atranscription_ignores_a_non_string_base_url_when_api_base_is_set
|
|||
|
||||
assert route.called
|
||||
assert response.text == "four score"
|
||||
|
||||
|
||||
def test_completion_marks_usage_missing_when_provider_omits_usage():
|
||||
# Regression test for #45586: a usage-less provider response must be
|
||||
# distinguishable from a real 0-token one.
|
||||
api_base: Final = "http://localhost:12347/v1"
|
||||
response_body: Final = {
|
||||
"id": "chatcmpl-missing-usage",
|
||||
"created": 1784657740,
|
||||
"model": "gpt-5.6",
|
||||
"object": "chat.completion",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "Hello"},
|
||||
}
|
||||
],
|
||||
}
|
||||
with respx.mock(assert_all_called=False) as router:
|
||||
mock_route: Final = router.post(url__regex=rf"{api_base}/chat/completions.*").mock(
|
||||
return_value=httpx.Response(status_code=200, json=response_body)
|
||||
)
|
||||
response: Final = litellm.completion(
|
||||
model="openai/gpt-5.6",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
api_base=api_base,
|
||||
api_key="fake_openai_api_key",
|
||||
)
|
||||
assert mock_route.called
|
||||
assert response.hidden_params["usage_missing"] is True
|
||||
# the zero-filled default stays intact for downstream consumers
|
||||
assert response.usage.prompt_tokens == 0
|
||||
assert response.usage.completion_tokens == 0
|
||||
|
||||
|
||||
def test_completion_real_zero_usage_is_not_marked_missing():
|
||||
api_base: Final = "http://localhost:12348/v1"
|
||||
response_body: Final = {
|
||||
"id": "chatcmpl-zero-usage",
|
||||
"created": 1784657740,
|
||||
"model": "gpt-5.6",
|
||||
"object": "chat.completion",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": ""},
|
||||
}
|
||||
],
|
||||
"usage": {"completion_tokens": 0, "prompt_tokens": 0, "total_tokens": 0},
|
||||
}
|
||||
with respx.mock(assert_all_called=False) as router:
|
||||
mock_route: Final = router.post(url__regex=rf"{api_base}/chat/completions.*").mock(
|
||||
return_value=httpx.Response(status_code=200, json=response_body)
|
||||
)
|
||||
response: Final = litellm.completion(
|
||||
model="openai/gpt-5.6",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
api_base=api_base,
|
||||
api_key="fake_openai_api_key",
|
||||
)
|
||||
assert mock_route.called
|
||||
assert "usage_missing" not in response.hidden_params
|
||||
assert response.usage.prompt_tokens == 0
|
||||
assert response.usage.completion_tokens == 0
|
||||
assert response.usage.total_tokens == 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue