mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-09 08:18:12 +00:00
fix(rate-limit): keep the slot owner consistent for unmapped MCP tools and pass-through bodies
A tool call that failed before its logging data was built, such as a tool name the gateway does not serve, reached the failure hook with no call id and still freed its parent's slot. The tool call's own id is now set right after the call is read. A pass-through body that carried its own litellm_call_id made that value the slot's owner, so the failure hook, which sees the proxy's id, skipped the release and the slot stayed taken for an hour. The limiter no longer takes an owner from a pass-through body.
This commit is contained in:
parent
d041c0ffe1
commit
d259ea474d
5 changed files with 370 additions and 774 deletions
|
|
@ -616,7 +616,7 @@ class _PROXY_DynamicRateLimitHandlerV3(CustomLogger):
|
|||
if "model" not in data:
|
||||
return None
|
||||
|
||||
claim_request_stash_for_data(data)
|
||||
claim_request_stash_for_data(data, call_type)
|
||||
model: Final = data["model"]
|
||||
priority: Final = self._get_priority_from_user_api_key_dict(user_api_key_dict=user_api_key_dict)
|
||||
|
||||
|
|
|
|||
|
|
@ -785,10 +785,10 @@ def get_or_create_request_stash() -> RequestRateLimiterStash:
|
|||
return stash
|
||||
|
||||
|
||||
def claim_request_stash_for_data(data: dict) -> RequestRateLimiterStash:
|
||||
def claim_request_stash_for_data(data: dict, call_type: str | None) -> RequestRateLimiterStash:
|
||||
stash: Final = get_or_create_request_stash()
|
||||
owner_call_id: Final = data.get("litellm_call_id")
|
||||
if isinstance(owner_call_id, str):
|
||||
if isinstance(owner_call_id, str) and call_type != CallTypes.pass_through.value:
|
||||
stash.owner_litellm_call_id = owner_call_id
|
||||
return stash
|
||||
|
||||
|
|
@ -3984,7 +3984,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
|
|||
"""
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
stash: Final = claim_request_stash_for_data(data)
|
||||
stash: Final = claim_request_stash_for_data(data, call_type)
|
||||
io_token_descriptors: Final = [
|
||||
d for d in descriptors if d["key"] in (PROJECT_ITPM_DESCRIPTOR_KEY, PROJECT_OTPM_DESCRIPTOR_KEY)
|
||||
]
|
||||
|
|
@ -4212,7 +4212,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
|
|||
"""
|
||||
verbose_proxy_logger.debug("Inside Rate Limit Pre-Call Hook")
|
||||
|
||||
stash: Final = claim_request_stash_for_data(data)
|
||||
stash: Final = claim_request_stash_for_data(data, call_type)
|
||||
reuses_request_slot: Final = call_type == CallTypes.call_mcp_tool.value and stash.parallel_slot is not None
|
||||
if reuses_request_slot:
|
||||
stash.parallel_slot_reused_at = self._get_current_time()
|
||||
|
|
|
|||
|
|
@ -742,6 +742,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
tool_arguments,
|
||||
tool_call_id,
|
||||
) = LiteLLM_Proxy_MCP_Handler.extract_tool_call_details(tool_call)
|
||||
tool_logging_call_id = (
|
||||
f"{litellm_call_id}_{tool_call_id or uuid.uuid4()}" if litellm_call_id else str(uuid.uuid4())
|
||||
)
|
||||
logging_request_data = {"litellm_call_id": tool_logging_call_id}
|
||||
|
||||
if not tool_name:
|
||||
verbose_logger.warning("Tool call missing name: %s", tool_call)
|
||||
|
|
@ -772,9 +776,6 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
},
|
||||
}
|
||||
]
|
||||
tool_logging_call_id = (
|
||||
f"{litellm_call_id}_{tool_call_id or uuid.uuid4()}" if litellm_call_id else str(uuid.uuid4())
|
||||
)
|
||||
logging_metadata: dict[str, object] = {
|
||||
"tool_call_id": tool_call_id,
|
||||
"tool_name": sanitized_tool_name,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -419,6 +419,25 @@ async def test_execute_tool_calls_logs_failure_via_post_call_failure_hook(monkey
|
|||
assert post_call_failure_hook.await_args.kwargs.get("route") == "/responses/mcp/call_tool"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_calls_reports_an_unmapped_tool_failure_under_the_tool_calls_own_id(monkeypatch):
|
||||
call_tool_mock: Final = _setup_mcp_call_environment(monkeypatch)
|
||||
post_call_failure_hook: Final = _setup_proxy_logging(monkeypatch)
|
||||
|
||||
results: Final = await LiteLLM_Proxy_MCP_Handler.execute_tool_calls(
|
||||
tool_server_map={},
|
||||
tool_calls=[{"id": "call-unmapped", "function": {"name": "unknown_tool", "arguments": "{}"}}],
|
||||
user_api_key_auth=types.SimpleNamespace(api_key="test_key", user_id="test_user"),
|
||||
litellm_call_id="cid",
|
||||
)
|
||||
|
||||
assert results[0]["result"].startswith("Error executing tool")
|
||||
call_tool_mock.assert_not_awaited()
|
||||
post_call_failure_hook.assert_awaited_once()
|
||||
assert post_call_failure_hook.await_args is not None
|
||||
assert post_call_failure_hook.await_args.kwargs["request_data"]["litellm_call_id"] == "cid_call-unmapped"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_calls_logs_each_tool_call_under_its_own_id_on_the_request_trace(
|
||||
monkeypatch,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue