mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
fix(responses): unwrap and authorize prompt_cache_options.comparison_response_id like previous_response_id
The proxy now decrypts an encrypted comparison_response_id and applies the same user and team ownership check previous_response_id gets, on top-level and extra_body prompt_cache_options. The native OpenAI Responses transform decodes the routing wrapper so OpenAI receives its own response id. Raw ids pass through unchanged Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
b92222922a
commit
1bbb32b697
3 changed files with 72 additions and 1 deletions
|
|
@ -15,7 +15,7 @@ from litellm.integrations.anthropic_cache_control_hook import (
|
|||
openai_prompt_cache_breakpoint_support,
|
||||
targets_openai_api,
|
||||
)
|
||||
from litellm.litellm_core_utils.core_helpers import normalize_drop_params, process_response_headers
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping, normalize_drop_params, process_response_headers
|
||||
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
safe_convert_created_field,
|
||||
|
|
@ -44,6 +44,7 @@ from litellm.responses.litellm_completion_transformation.tool_search import (
|
|||
tools_with_anthropic_tool_search_mapped,
|
||||
tools_without_anthropic_tool_search,
|
||||
)
|
||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
from litellm.types.llms.openai import *
|
||||
|
|
@ -623,11 +624,39 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
"model": model,
|
||||
"input": replay_safe_input,
|
||||
**response_api_optional_request_params,
|
||||
**self._prompt_cache_options_with_original_comparison_id(request_params),
|
||||
},
|
||||
)
|
||||
|
||||
return final_request_params
|
||||
|
||||
def transform_extra_body(
|
||||
self,
|
||||
extra_body: Mapping[str, object],
|
||||
request: Mapping[str, object],
|
||||
model: str,
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
) -> Mapping[str, object]:
|
||||
return {**extra_body, **self._prompt_cache_options_with_original_comparison_id(extra_body)}
|
||||
|
||||
def _prompt_cache_options_with_original_comparison_id(self, params: Mapping[str, object]) -> Mapping[str, object]:
|
||||
options: Final = as_str_mapping(params.get("prompt_cache_options"))
|
||||
comparison_response_id: Final = None if options is None else options.get("comparison_response_id")
|
||||
if (
|
||||
self.custom_llm_provider != LlmProviders.OPENAI
|
||||
or options is None
|
||||
or not isinstance(comparison_response_id, str)
|
||||
):
|
||||
return {}
|
||||
original_response_id: Final = (
|
||||
ResponsesAPIRequestUtils.decode_previous_response_id_to_original_previous_response_id(
|
||||
comparison_response_id
|
||||
)
|
||||
)
|
||||
if original_response_id == comparison_response_id:
|
||||
return {}
|
||||
return {"prompt_cache_options": {**options, "comparison_response_id": original_response_id}}
|
||||
|
||||
def _prepared_input_and_tools(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from pydantic import TypeAdapter, ValidationError
|
|||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
|
||||
decrypt_value_helper,
|
||||
|
|
@ -135,6 +136,9 @@ class ResponsesIDSecurity(CustomLogger):
|
|||
addressed_id_field: Final = _ADDRESSED_RESPONSE_ID_FIELD_BY_CALL_TYPE.get(call_type)
|
||||
if addressed_id_field is None:
|
||||
return None
|
||||
if call_type == "aresponses":
|
||||
request_data: Final = cast("dict[str, object]", data) # cast-ok: the hook signature leaves the body untyped
|
||||
request_data.update(self._authorized_comparison_response_id_fields(request_data, user_api_key_dict))
|
||||
retained_id: Final = data.get(ADDRESSED_RESPONSE_ID_FIELD)
|
||||
addressed_id: Final = (
|
||||
retained_id if isinstance(retained_id, str) and retained_id else data.get(addressed_id_field)
|
||||
|
|
@ -146,6 +150,43 @@ class ResponsesIDSecurity(CustomLogger):
|
|||
data[ADDRESSED_RESPONSE_ID_FIELD] = addressed_id
|
||||
return data
|
||||
|
||||
def _authorized_comparison_response_id_fields(
|
||||
self,
|
||||
data: Mapping[str, object],
|
||||
user_api_key_dict: "UserAPIKeyAuth",
|
||||
) -> Mapping[str, object]:
|
||||
options: Final = self._authorized_prompt_cache_options(data.get("prompt_cache_options"), user_api_key_dict)
|
||||
extra_body: Final = as_str_mapping(data.get("extra_body"))
|
||||
extra_body_options: Final = (
|
||||
None
|
||||
if extra_body is None
|
||||
else self._authorized_prompt_cache_options(extra_body.get("prompt_cache_options"), user_api_key_dict)
|
||||
)
|
||||
options_field: Final[Mapping[str, object]] = {} if options is None else {"prompt_cache_options": options}
|
||||
extra_body_field: Final[Mapping[str, object]] = (
|
||||
{}
|
||||
if extra_body is None or extra_body_options is None
|
||||
else {"extra_body": {**extra_body, "prompt_cache_options": extra_body_options}}
|
||||
)
|
||||
return {**options_field, **extra_body_field}
|
||||
|
||||
def _authorized_prompt_cache_options(
|
||||
self,
|
||||
prompt_cache_options: object,
|
||||
user_api_key_dict: "UserAPIKeyAuth",
|
||||
) -> Mapping[str, object] | None:
|
||||
options: Final = as_str_mapping(prompt_cache_options)
|
||||
comparison_response_id: Final = None if options is None else options.get("comparison_response_id")
|
||||
if (
|
||||
options is None
|
||||
or not isinstance(comparison_response_id, str)
|
||||
or not self._is_encrypted_response_id(comparison_response_id)
|
||||
):
|
||||
return None
|
||||
original_response_id, user_id, team_id = self._decrypt_response_id(comparison_response_id)
|
||||
self.check_user_access_to_response_id(user_id, team_id, user_api_key_dict)
|
||||
return {**options, "comparison_response_id": original_response_id}
|
||||
|
||||
def authorize_response_id(
|
||||
self,
|
||||
response_id: str,
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ class PromptCacheBreakpoint(TypedDict):
|
|||
class PromptCacheOptions(TypedDict, total=False):
|
||||
mode: ReadOnly[Literal["implicit", "explicit"]]
|
||||
ttl: ReadOnly[Literal["30m", "12h"]]
|
||||
comparison_response_id: ReadOnly[str]
|
||||
|
||||
|
||||
class ChatCompletionThinkingBlock(TypedDict, total=False):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue