mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
fix(gemini): pair a tool result with its call when the client re-adds the thought signature to the call id
Strands, which the AgentCore harness runs, puts provider_specific_fields.thought_signature back on the assistant tool call id as <id>__thought__<signature> while its tool result keeps the bare id, so the Gemini conversion found no matching call and answered 500. Ids now match with the signature suffix stripped. Also adds thought_signature_in_tool_call_id to the litellm_params inventory test and gives the pass-through tests' mocked logging objects the litellm_params a real Logging object always has.
This commit is contained in:
parent
c535022057
commit
d26b776e5e
5 changed files with 61 additions and 1 deletions
|
|
@ -1261,6 +1261,13 @@ def encode_tool_call_id_with_signature(tool_call_id: str, thought_signature: str
|
|||
_encode_tool_call_id_with_signature = encode_tool_call_id_with_signature
|
||||
|
||||
|
||||
def _same_tool_call(tool_call_id: str, other_tool_call_id: str) -> bool:
|
||||
return (
|
||||
tool_call_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1)[0]
|
||||
== other_tool_call_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1)[0]
|
||||
)
|
||||
|
||||
|
||||
def embeds_thought_signature_in_tool_call_id(litellm_params: Mapping[str, object]) -> bool:
|
||||
return litellm_params.get("thought_signature_in_tool_call_id") is not False
|
||||
|
||||
|
|
@ -1609,7 +1616,13 @@ def convert_to_gemini_tool_call_result(
|
|||
msg_tool_call_id: Final = message.get("tool_call_id", None)
|
||||
for tool in tools:
|
||||
prev_tool_call_id = tool.get("id", None)
|
||||
if msg_tool_call_id and prev_tool_call_id and msg_tool_call_id == prev_tool_call_id:
|
||||
if (
|
||||
isinstance(msg_tool_call_id, str)
|
||||
and isinstance(prev_tool_call_id, str)
|
||||
and msg_tool_call_id
|
||||
and prev_tool_call_id
|
||||
and _same_tool_call(msg_tool_call_id, prev_tool_call_id)
|
||||
):
|
||||
name = tool.get("function", {}).get("name", "")
|
||||
|
||||
# Echo the OpenAI tool_call_id on functionResponse (strip thought-signature suffix).
|
||||
|
|
|
|||
|
|
@ -430,3 +430,47 @@ def test_bounded_tool_call_id_round_trips_to_gemini(client_keeps_provider_fields
|
|||
assert len(replayed_signatures) == 1
|
||||
assert (replayed_signatures[0] == LONG_SIGNATURE) is client_keeps_provider_fields
|
||||
assert replayed_signatures[0]
|
||||
|
||||
|
||||
def test_tool_result_with_bare_id_matches_call_whose_id_the_client_re_signed():
|
||||
bare_id: Final = "call_601401"
|
||||
with respx.mock:
|
||||
route: Final = respx.post(url__regex=GEMINI_URL_PATTERN).mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"candidates": [
|
||||
{"content": {"role": "model", "parts": [{"text": "Sunny"}]}, "finishReason": "STOP", "index": 0}
|
||||
],
|
||||
"usageMetadata": {"promptTokenCount": 9, "candidatesTokenCount": 1, "totalTokenCount": 10},
|
||||
},
|
||||
)
|
||||
)
|
||||
litellm.completion(
|
||||
model="gemini/gemini-3.5-flash",
|
||||
messages=[
|
||||
USER_TURN,
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": encode_tool_call_id_with_signature(bare_id, LONG_SIGNATURE),
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "arguments": '{"city": "Paris"}'},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": bare_id, "content": '{"temp_c": 21}'},
|
||||
],
|
||||
tools=[WEATHER_TOOL],
|
||||
api_key="test-gemini-key",
|
||||
thought_signature_in_tool_call_id=False,
|
||||
)
|
||||
sent_body: Final = json.loads(route.calls.last.request.content)
|
||||
|
||||
parts: Final = list(chain.from_iterable(turn["parts"] for turn in sent_body["contents"]))
|
||||
function_calls: Final = [part for part in parts if "function_call" in part]
|
||||
function_responses: Final = [part["function_response"] for part in parts if "function_response" in part]
|
||||
assert [part["thoughtSignature"] for part in function_calls] == [LONG_SIGNATURE]
|
||||
assert [response["name"] for response in function_responses] == ["get_weather"]
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class TestGeminiPassthroughLoggingHandler:
|
|||
mock_logging_obj = MagicMock(spec=LiteLLMLoggingObj)
|
||||
mock_logging_obj.model_call_details = {}
|
||||
mock_logging_obj.optional_params = {}
|
||||
mock_logging_obj.litellm_params = {}
|
||||
mock_logging_obj.litellm_call_id = "test-call-id-123"
|
||||
return mock_logging_obj
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ def _logging_obj() -> LiteLLMLoggingObj:
|
|||
logging_obj = MagicMock(spec=LiteLLMLoggingObj)
|
||||
logging_obj.model_call_details = {}
|
||||
logging_obj.optional_params = {}
|
||||
logging_obj.litellm_params = {}
|
||||
logging_obj.litellm_call_id = "test-call-id"
|
||||
return logging_obj
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ OPTION_NAMES: Final = (
|
|||
"assistant_continue_message",
|
||||
"disable_add_transform_inline_image_block",
|
||||
"merge_reasoning_content_in_choices",
|
||||
"thought_signature_in_tool_call_id",
|
||||
"enable_json_schema_validation",
|
||||
"complete_response",
|
||||
"stream_chunk_size",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue