Repository navigation
chore(release): backport #40989 to stable/1.101.x and cut 1.101.6 #45212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| from collections.abc import Sequence | ||
| from dataclasses import dataclass | ||
| from typing import Final, cast # noqa: TID251 # validating the openai tool union strips vendor keys from raw tools | ||
|
|
||
| from pydantic import BaseModel, ValidationError | ||
|
|
||
| from litellm._logging import verbose_logger | ||
| from litellm.types.llms.openai import ALL_RESPONSES_API_TOOL_PARAMS, ResponseInputParam | ||
|
|
||
| ADDITIONAL_TOOLS_INPUT_ITEM_TYPE: Final = "additional_tools" | ||
|
|
||
|
|
||
| class _InputItemType(BaseModel): | ||
| type: str = "" | ||
|
|
||
|
|
||
| class _AdditionalToolsItem(BaseModel): | ||
| tools: tuple[dict[str, object], ...] = () | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class HoistedAdditionalTools: | ||
| input: str | ResponseInputParam | ||
| tools: tuple[ALL_RESPONSES_API_TOOL_PARAMS, ...] | ||
| hoisted: tuple[ALL_RESPONSES_API_TOOL_PARAMS, ...] | ||
|
|
||
|
|
||
| def _is_additional_tools_item(item: object) -> bool: | ||
| try: | ||
| return _InputItemType.model_validate(item).type == ADDITIONAL_TOOLS_INPUT_ITEM_TYPE | ||
| except ValidationError: | ||
| return False | ||
|
|
||
|
|
||
| def _tools_of_item(item: object) -> tuple[ALL_RESPONSES_API_TOOL_PARAMS, ...]: | ||
| try: | ||
| parsed: Final = _AdditionalToolsItem.model_validate(item) | ||
| except ValidationError: | ||
| return () | ||
| return tuple( | ||
| cast( | ||
| "ALL_RESPONSES_API_TOOL_PARAMS", tool | ||
| ) # cast-ok: nested tools carry the same raw tool JSON as top-level tools | ||
| for tool in parsed.tools | ||
| ) | ||
|
|
||
|
|
||
| def hoist_additional_tools( | ||
| input: str | ResponseInputParam, | ||
| tools: Sequence[ALL_RESPONSES_API_TOOL_PARAMS] | None, | ||
| ) -> HoistedAdditionalTools: | ||
| existing: Final = tuple(tools or ()) | ||
| if isinstance(input, str): | ||
| return HoistedAdditionalTools(input=input, tools=existing, hoisted=()) | ||
| items: Final = tuple(item for item in input if _is_additional_tools_item(item)) | ||
| if not items: | ||
| return HoistedAdditionalTools(input=input, tools=existing, hoisted=()) | ||
| hoisted: Final = tuple(tool for item in items for tool in _tools_of_item(item)) | ||
| verbose_logger.debug( | ||
| "Responses API: hoisting %d tool(s) out of %d 'additional_tools' input item(s) into the top-level tools param.", | ||
| len(hoisted), | ||
| len(items), | ||
| ) | ||
| remaining_input: Final = [item for item in input if not _is_additional_tools_item(item)] | ||
| return HoistedAdditionalTools(input=remaining_input, tools=(*existing, *hoisted), hoisted=hoisted) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| from typing import Final | ||
|
|
||
| import litellm | ||
| from litellm.responses.additional_tools import hoist_additional_tools | ||
| from litellm.responses.litellm_completion_transformation.streaming_iterator import ( | ||
| LiteLLMCompletionStreamingIterator, | ||
| ) | ||
|
|
@@ -37,11 +38,16 @@ def response_api_handler( | |
| | BaseResponsesAPIStreamingIterator | ||
| | Coroutine[object, object, ResponsesAPIResponse | BaseResponsesAPIStreamingIterator] | ||
| ): | ||
| hoisted: Final = hoist_additional_tools(input, responses_api_request.get("tools")) | ||
| bridged_input: Final = hoisted.input | ||
| bridged_request: Final[ResponsesAPIOptionalRequestParams] = ( | ||
| {**responses_api_request, "tools": list(hoisted.tools)} if hoisted.hoisted else responses_api_request | ||
|
Comment on lines
+41
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With How this was verified: The request path checks top-level tools before hoisting, then skips the already-run guardrail when calling completion |
||
| ) | ||
| litellm_completion_request: Final[dict] = ( | ||
| LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( | ||
| model=model, | ||
| input=input, | ||
| responses_api_request=responses_api_request, | ||
| input=bridged_input, | ||
| responses_api_request=bridged_request, | ||
| custom_llm_provider=custom_llm_provider, | ||
| stream=stream, | ||
| extra_headers=extra_headers, | ||
|
|
@@ -52,8 +58,8 @@ def response_api_handler( | |
| if _is_async: | ||
| return self.async_response_api_handler( | ||
| litellm_completion_request=litellm_completion_request, | ||
| request_input=input, | ||
| responses_api_request=responses_api_request, | ||
| request_input=bridged_input, | ||
| responses_api_request=bridged_request, | ||
| **kwargs, | ||
| ) | ||
|
|
||
|
|
@@ -70,8 +76,8 @@ def response_api_handler( | |
| responses_api_response: Final[ResponsesAPIResponse] = ( | ||
| LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( | ||
| chat_completion_response=litellm_completion_response, | ||
| request_input=input, | ||
| responses_api_request=responses_api_request, | ||
| request_input=bridged_input, | ||
| responses_api_request=bridged_request, | ||
| ) | ||
| ) | ||
|
|
||
|
|
@@ -81,8 +87,8 @@ def response_api_handler( | |
| return LiteLLMCompletionStreamingIterator( | ||
| model=model, | ||
| litellm_custom_stream_wrapper=litellm_completion_response, | ||
| request_input=input, | ||
| responses_api_request=responses_api_request, | ||
| request_input=bridged_input, | ||
| responses_api_request=bridged_request, | ||
| custom_llm_provider=custom_llm_provider, | ||
| litellm_metadata=kwargs.get("litellm_metadata", {}), | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium: Late tool hoisting bypasses key and team allowlists
On a Responses-to-Chat-Completions deployment, a caller whose key/team
allowed_toolspermits onlyreadcan place awritefunction in aninputitem of typeadditional_tools, alongside a normal message, and have it forwarded to the provider.check_tools_allowlist()runs during authentication and its Responses extractor only reads top-leveltools, so this newly supported declaration is never authorized; hoist before that check, or reapply it to the merged tools using the authenticated key/team before callingcompletion/acompletion.