fix: handle item ids and function call ids between different providers

This commit is contained in:
Lok 2026-07-23 11:29:56 +01:00
parent 3bba3633c7
commit 047fb99de8
3 changed files with 173 additions and 0 deletions

View file

@ -1111,6 +1111,11 @@ def responses(
# Decode any litellm-encoded encrypted-content item IDs back to their original IDs
input = ResponsesAPIRequestUtils._restore_encrypted_content_item_ids_in_input(input)
input = ResponsesAPIRequestUtils._normalize_function_call_ids_in_input(
request_input=input,
model=model,
custom_llm_provider=custom_llm_provider,
)
# Call the handler with _is_async flag instead of directly calling the async handler
if custom_llm_provider is None:

View file

@ -463,6 +463,93 @@ class ResponsesAPIRequestUtils:
return request_input
@staticmethod
def _normalize_call_id_for_provider(
call_id: str,
model: str | None,
custom_llm_provider: str | None,
) -> str:
"""Strip Gemini thought signatures from call_id when replaying to non-Gemini models."""
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
)
from litellm.utils import _is_gemini_model, _remove_thought_signature_from_id
if _is_gemini_model(model=model, custom_llm_provider=custom_llm_provider):
return call_id
return _remove_thought_signature_from_id(call_id, THOUGHT_SIGNATURE_SEPARATOR)
@staticmethod
def _normalize_function_call_item_id_for_provider(
item_id: str,
model: str | None,
custom_llm_provider: str | None,
) -> str:
"""Rewrite foreign provider function_call item ids to OpenAI fc_ format."""
item_id = ResponsesAPIRequestUtils._normalize_call_id_for_provider(
call_id=item_id,
model=model,
custom_llm_provider=custom_llm_provider,
)
if custom_llm_provider != "openai":
return item_id
if item_id.startswith("call_"):
return f"fc_{item_id[len('call_') :]}"
if item_id.startswith("tooluse_"):
return f"fc_{item_id[len('tooluse_') :]}"
if item_id.startswith("toolu_vrtx_"):
return f"fc_{item_id[len('toolu_vrtx_') :]}"
return item_id
@staticmethod
def _normalize_function_call_ids_in_input(
request_input: Any,
model: str | None,
custom_llm_provider: str | None,
) -> Any:
"""Normalize function_call / function_call_output IDs before upstream replay.
- Strips Gemini thought signatures from call_id for non-Gemini targets.
- Rewrites foreign function_call item ids (call_, tooluse_) to fc_ for OpenAI-compatible targets.
"""
if not isinstance(request_input, list):
return request_input
for item in request_input:
if not isinstance(item, dict):
continue
item_type = item.get("type")
if item_type == "function_call":
call_id = item.get("call_id")
if call_id and isinstance(call_id, str):
item["call_id"] = ResponsesAPIRequestUtils._normalize_call_id_for_provider(
call_id=call_id,
model=model,
custom_llm_provider=custom_llm_provider,
)
item_id = item.get("id")
if item_id and isinstance(item_id, str):
item["id"] = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider(
item_id=item_id,
model=model,
custom_llm_provider=custom_llm_provider,
)
elif item_type == "function_call_output":
call_id = item.get("call_id")
if call_id and isinstance(call_id, str):
item["call_id"] = ResponsesAPIRequestUtils._normalize_call_id_for_provider(
call_id=call_id,
model=model,
custom_llm_provider=custom_llm_provider,
)
return request_input
@staticmethod
def _build_responses_api_response_id(
custom_llm_provider: Optional[str],

View file

@ -228,6 +228,87 @@ class TestResponsesAPIRequestUtils:
assert decoded.get("custom_llm_provider") == "azure"
assert decoded.get("response_id") == "cntr_x"
def test_normalize_call_id_strips_thought_signature_for_non_gemini(self):
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
)
call_id = f"call_abc123{THOUGHT_SIGNATURE_SEPARATOR}sig_xyz"
result = ResponsesAPIRequestUtils._normalize_call_id_for_provider(
call_id=call_id,
model="gpt-4o",
custom_llm_provider="openai",
)
assert result == "call_abc123"
def test_normalize_call_id_preserves_thought_signature_for_gemini(self):
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
)
call_id = f"call_abc123{THOUGHT_SIGNATURE_SEPARATOR}sig_xyz"
result = ResponsesAPIRequestUtils._normalize_call_id_for_provider(
call_id=call_id,
model="gemini-2.5-flash",
custom_llm_provider="gemini",
)
assert result == call_id
def test_normalize_function_call_item_id_rewrites_for_openai(self):
result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider(
item_id="call_abc123",
model="gpt-4o",
custom_llm_provider="openai",
)
assert result == "fc_abc123"
result_tooluse = (
ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider(
item_id="tooluse_abc123",
model="gpt-4o",
custom_llm_provider="openai",
)
)
assert result_tooluse == "fc_abc123"
def test_normalize_function_call_item_id_no_rewrite_for_anthropic(self):
result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider(
item_id="call_abc123",
model="claude-3-5-sonnet",
custom_llm_provider="anthropic",
)
assert result == "call_abc123"
def test_normalize_function_call_ids_in_input(self):
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
)
request_input = [
{
"type": "function_call",
"id": "tooluse_xyz",
"call_id": f"call_abc{THOUGHT_SIGNATURE_SEPARATOR}sig",
"name": "get_weather",
"arguments": "{}",
},
{
"type": "function_call_output",
"call_id": f"call_abc{THOUGHT_SIGNATURE_SEPARATOR}sig",
"output": "sunny",
},
]
result = ResponsesAPIRequestUtils._normalize_function_call_ids_in_input(
request_input=request_input,
model="gpt-4o",
custom_llm_provider="openai",
)
assert result[0]["id"] == "fc_xyz"
assert result[0]["call_id"] == "call_abc"
assert result[1]["call_id"] == "call_abc"
class TestResponseAPILoggingUtils:
def test_is_response_api_usage_true(self):