This commit is contained in:
Phani Paladugula 2026-11-07 05:49:33 +00:00 • committed by GitHub
commit 28b2cf8cf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 93 additions and 1 deletions

View file

@ -1067,6 +1067,7 @@ class LLMCachingHandler:
cached_result = convert_to_model_response_object(
response_object=cached_result,
model_response_object=ModelResponse(),
_preserve_finish_reason=True,
)
if (
call_type == CallTypes.atext_completion.value or call_type == CallTypes.text_completion.value
@ -1140,6 +1141,7 @@ class LLMCachingHandler:
cached_result = convert_to_model_response_object(
response_object=cached_result,
model_response_object=ModelResponse(),
_preserve_finish_reason=True,
)
else:
from litellm.responses.streaming_iterator import (

View file

@ -747,6 +747,7 @@ def convert_to_model_response_object(
hidden_params: dict | None = None,
_response_headers: dict | None = None,
convert_tool_call_to_json_mode: bool | None = None, # used for supporting 'json_schema' on older models
_preserve_finish_reason: bool = False, # cached responses already have their final finish_reason
):
additional_headers = get_response_headers(_response_headers)
@ -888,7 +889,12 @@ def convert_to_model_response_object(
if finish_reason is None:
# gpt-4 vision can return 'finish_reason' or 'finish_details'
finish_reason = choice.get("finish_details") or "stop"
if finish_reason == "stop" and message.tool_calls and len(message.tool_calls) > 0:
if (
not _preserve_finish_reason
and finish_reason == "stop"
and message.tool_calls
and len(message.tool_calls) > 0
):
finish_reason = "tool_calls"
## PROVIDER SPECIFIC FIELDS ##

View file

@ -2052,6 +2052,90 @@ def test_convert_cached_responses_bridge_chat_completion_nonstream():
assert result.choices[0].message.content == "Hi!"
def test_convert_cached_completion_preserves_stored_finish_reason():
"""A cache hit must replay the stored finish_reason, not re-derive it from tool_calls."""
caching_handler = LLMCachingHandler(
original_function=completion, request_kwargs={}, start_time=datetime.now()
)
cached_result = {
"id": "chatcmpl-cache-finish-reason",
"object": "chat.completion",
"created": int(time.time()),
"model": "claude-sonnet-4-5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "toolu_1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{}"},
}
],
},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
result = caching_handler._convert_cached_result_to_model_response(
cached_result=cached_result,
call_type=CallTypes.completion.value,
kwargs={
"model": "claude-sonnet-4-5",
"stream": False,
"messages": [{"role": "user", "content": "weather?"}],
},
logging_obj=_build_logging_obj(CallTypes.completion.value, stream=False),
model="claude-sonnet-4-5",
args=(),
)
assert isinstance(result, ModelResponse)
assert result.choices[0].message.tool_calls is not None
assert result.choices[0].finish_reason == "stop"
def test_convert_raw_completion_still_derives_finish_reason_from_tool_calls():
"""Provider payloads that say 'stop' but carry tool calls still map to 'tool_calls'."""
from litellm.utils import convert_to_model_response_object
result = convert_to_model_response_object(
response_object={
"id": "chatcmpl-raw-finish-reason",
"object": "chat.completion",
"created": int(time.time()),
"model": "claude-sonnet-4-5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "toolu_1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{}"},
}
],
},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
model_response_object=ModelResponse(),
)
assert result is not None
assert result.choices[0].finish_reason == "tool_calls"
def test_convert_cached_responses_legacy_nonstream_path():
"""Genuine ResponsesAPIResponse dict (no chatcmpl/choices) falls through legacy path."""
from litellm import responses