diff --git a/litellm/llms/azure/responses/transformation.py b/litellm/llms/azure/responses/transformation.py index 7d9244e31bc2..ae14d6ef4f0f 100644 --- a/litellm/llms/azure/responses/transformation.py +++ b/litellm/llms/azure/responses/transformation.py @@ -170,3 +170,35 @@ def transform_get_response_api_request( data: Dict = {} verbose_logger.debug(f"get response url={get_url}") return get_url, data + + def transform_list_input_items_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + ) -> Tuple[str, Dict]: + url = ( + self._construct_url_for_response_id_in_path( + api_base=api_base, response_id=response_id + ) + + "/input_items" + ) + params: Dict[str, Any] = {} + if after is not None: + params["after"] = after + if before is not None: + params["before"] = before + if include: + params["include"] = ",".join(include) + if limit is not None: + params["limit"] = limit + if order is not None: + params["order"] = order + verbose_logger.debug(f"list input items url={url}") + return url, params diff --git a/litellm/llms/base_llm/responses/transformation.py b/litellm/llms/base_llm/responses/transformation.py index 751d29dd5634..b2a555086d85 100644 --- a/litellm/llms/base_llm/responses/transformation.py +++ b/litellm/llms/base_llm/responses/transformation.py @@ -156,7 +156,7 @@ def transform_get_response_api_request( headers: dict, ) -> Tuple[str, Dict]: pass - + @abstractmethod def transform_get_response_api_response( self, @@ -165,10 +165,36 @@ def transform_get_response_api_response( ) -> ResponsesAPIResponse: pass + ######################################################### + ########## LIST INPUT ITEMS API TRANSFORMATION ########## + ######################################################### + @abstractmethod + def transform_list_input_items_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + ) -> Tuple[str, Dict]: + pass + + @abstractmethod + def transform_list_input_items_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> Dict: + pass + ######################################################### ########## END GET RESPONSE API TRANSFORMATION ########## ######################################################### - + def get_error_class( self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] ) -> BaseLLMException: diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 2d337c5c9ef9..d1c68a6dccd5 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -6,6 +6,7 @@ Coroutine, Dict, List, + Literal, Optional, Tuple, Union, @@ -1812,6 +1813,168 @@ async def async_get_responses( logging_obj=logging_obj, ) + ##################################################################### + ################ LIST RESPONSES INPUT ITEMS HANDLER ########################### + ##################################################################### + def list_responses_input_items( + self, + response_id: str, + responses_api_provider_config: BaseResponsesAPIConfig, + litellm_params: GenericLiteLLMParams, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + extra_headers: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + _is_async: bool = False, + ) -> Union[Dict, Coroutine[Any, Any, Dict]]: + if _is_async: + return self.async_list_responses_input_items( + response_id=response_id, + responses_api_provider_config=responses_api_provider_config, + litellm_params=litellm_params, + logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, + after=after, + before=before, + include=include, + limit=limit, + order=order, + extra_headers=extra_headers, + timeout=timeout, + client=client, + ) + + if client is None or not isinstance(client, HTTPHandler): + sync_httpx_client = _get_httpx_client( + params={"ssl_verify": litellm_params.get("ssl_verify", None)} + ) + else: + sync_httpx_client = client + + headers = responses_api_provider_config.validate_environment( + api_key=litellm_params.api_key, + headers=extra_headers or {}, + model="None", + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = responses_api_provider_config.get_complete_url( + api_base=litellm_params.api_base, + litellm_params=dict(litellm_params), + ) + + url, params = responses_api_provider_config.transform_list_input_items_request( + response_id=response_id, + api_base=api_base, + litellm_params=litellm_params, + headers=headers, + after=after, + before=before, + include=include, + limit=limit, + order=order, + ) + + logging_obj.pre_call( + input="", + api_key="", + additional_args={ + "complete_input_dict": params, + "api_base": api_base, + "headers": headers, + }, + ) + + try: + response = sync_httpx_client.get(url=url, headers=headers, params=params) + except Exception as e: + raise self._handle_error(e=e, provider_config=responses_api_provider_config) + + return responses_api_provider_config.transform_list_input_items_response( + raw_response=response, + logging_obj=logging_obj, + ) + + async def async_list_responses_input_items( + self, + response_id: str, + responses_api_provider_config: BaseResponsesAPIConfig, + litellm_params: GenericLiteLLMParams, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + extra_headers: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + ) -> Dict: + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + headers = responses_api_provider_config.validate_environment( + api_key=litellm_params.api_key, + headers=extra_headers or {}, + model="None", + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = responses_api_provider_config.get_complete_url( + api_base=litellm_params.api_base, + litellm_params=dict(litellm_params), + ) + + url, params = responses_api_provider_config.transform_list_input_items_request( + response_id=response_id, + api_base=api_base, + litellm_params=litellm_params, + headers=headers, + after=after, + before=before, + include=include, + limit=limit, + order=order, + ) + + logging_obj.pre_call( + input="", + api_key="", + additional_args={ + "complete_input_dict": params, + "api_base": api_base, + "headers": headers, + }, + ) + + try: + response = await async_httpx_client.get( + url=url, headers=headers, params=params + ) + except Exception as e: + raise self._handle_error(e=e, provider_config=responses_api_provider_config) + + return responses_api_provider_config.transform_list_input_items_response( + raw_response=response, + logging_obj=logging_obj, + ) + def create_file( self, create_file_data: CreateFileRequest, @@ -2134,7 +2297,10 @@ def image_edit_handler( _is_async: bool = False, fake_stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, - ) -> Union[ImageResponse, Coroutine[Any, Any, ImageResponse],]: + ) -> Union[ + ImageResponse, + Coroutine[Any, Any, ImageResponse], + ]: """ Handles image edit requests. diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index bdbdcf99fdc1..7b77ec1ef150 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -251,7 +251,7 @@ def transform_delete_response_api_response( message=raw_response.text, status_code=raw_response.status_code ) return DeleteResponseResult(**raw_response_json) - + ######################################################### ########## GET RESPONSE API TRANSFORMATION ############### ######################################################### @@ -271,7 +271,7 @@ def transform_get_response_api_request( url = f"{api_base}/{response_id}" data: Dict = {} return url, data - + def transform_get_response_api_response( self, raw_response: httpx.Response, @@ -287,3 +287,44 @@ def transform_get_response_api_response( message=raw_response.text, status_code=raw_response.status_code ) return ResponsesAPIResponse(**raw_response_json) + + ######################################################### + ########## LIST INPUT ITEMS TRANSFORMATION ############# + ######################################################### + def transform_list_input_items_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + ) -> Tuple[str, Dict]: + url = f"{api_base}/{response_id}/input_items" + params: Dict[str, Any] = {} + if after is not None: + params["after"] = after + if before is not None: + params["before"] = before + if include: + params["include"] = ",".join(include) + if limit is not None: + params["limit"] = limit + if order is not None: + params["order"] = order + return url, params + + def transform_list_input_items_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> Dict: + try: + return raw_response.json() + except Exception: + raise OpenAIError( + message=raw_response.text, status_code=raw_response.status_code + ) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 5207a3415540..e88637a67876 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -4153,6 +4153,32 @@ "supports_assistant_prefill": true, "supports_tool_choice": true }, + "mistral/magistral-medium-2506": { + "max_tokens": 40000, + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "input_cost_per_token": 2e-06, + "output_cost_per_token": 5e-06, + "litellm_provider": "mistral", + "mode": "chat", + "source": "https://mistral.ai/news/magistral", + "supports_function_calling": true, + "supports_assistant_prefill": true, + "supports_tool_choice": true + }, + "mistral/magistral-small-2506": { + "max_tokens": 40000, + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "mistral", + "mode": "chat", + "source": "https://mistral.ai/news/magistral", + "supports_function_calling": true, + "supports_assistant_prefill": true, + "supports_tool_choice": true + }, "mistral/mistral-embed": { "max_tokens": 8192, "max_input_tokens": 8192, diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 5c9b3576586c..829a495e9db9 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -250,6 +250,7 @@ async def common_processing_pre_call_logic( "acancel_fine_tuning_job", "alist_fine_tuning_jobs", "aretrieve_fine_tuning_job", + "alist_input_items", "aimage_edit", ], version: Optional[str] = None, @@ -329,6 +330,7 @@ async def base_process_llm_request( "adelete_responses", "atext_completion", "aimage_edit", + "alist_input_items", ], proxy_logging_obj: ProxyLogging, general_settings: dict, diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index bef5b4b807e7..18481f11e2f4 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -240,15 +240,48 @@ async def get_response_input_items( fastapi_response: Response, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): - """ - Get input items for a response. - - Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses/input-items - - ```bash - curl -X GET http://localhost:4000/v1/responses/resp_abc123/input_items \ - -H "Authorization: Bearer sk-1234" - ``` - """ - # TODO: Implement input items retrieval logic - pass + """List input items for a response.""" + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["response_id"] = response_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="alist_input_items", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 0d8f04dbc570..970526afad60 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -22,6 +22,7 @@ "amoderation": "/moderations", "arerank": "/rerank", "aresponses": "/responses", + "alist_input_items": "/responses/{response_id}/input_items", "aimage_edit": "/images/edits", } @@ -69,6 +70,7 @@ async def route_request( "aresponses", "aget_responses", "adelete_responses", + "alist_input_items", "_arealtime", # private function for realtime API "aimage_edit", ], @@ -134,7 +136,12 @@ async def route_request( or len(llm_router.pattern_router.patterns) > 0 ): return getattr(llm_router, f"{route_type}")(**data) - elif route_type in ["amoderation", "aget_responses", "adelete_responses"]: + elif route_type in [ + "amoderation", + "aget_responses", + "adelete_responses", + "alist_input_items", + ]: # moderation endpoint does not require `model` parameter return getattr(llm_router, f"{route_type}")(**data) diff --git a/litellm/responses/main.py b/litellm/responses/main.py index bd0c7246c07b..fded67285fd4 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -435,6 +435,7 @@ def delete_responses( extra_kwargs=kwargs, ) + @client async def aget_responses( response_id: str, @@ -450,13 +451,13 @@ async def aget_responses( ) -> ResponsesAPIResponse: """ Async: Fetch a response by its ID. - + GET /v1/responses/{response_id} endpoint in the responses API - + Args: response_id: The ID of the response to fetch. custom_llm_provider: Optional provider name. If not specified, will be decoded from response_id. - + Returns: The response object with complete information about the stored response. """ @@ -496,7 +497,7 @@ async def aget_responses( else: response = init_response - # Update the responses_api_response_id with the model_id + # Update the responses_api_response_id with the model_id if isinstance(response, ResponsesAPIResponse): response = ResponsesAPIRequestUtils._update_responses_api_response_id_with_model_id( responses_api_response=response, @@ -513,6 +514,7 @@ async def aget_responses( extra_kwargs=kwargs, ) + @client def get_responses( response_id: str, @@ -528,13 +530,13 @@ def get_responses( ) -> Union[ResponsesAPIResponse, Coroutine[Any, Any, ResponsesAPIResponse]]: """ Fetch a response by its ID. - + GET /v1/responses/{response_id} endpoint in the responses API - + Args: response_id: The ID of the response to fetch. custom_llm_provider: Optional provider name. If not specified, will be decoded from response_id. - + Returns: The response object with complete information about the stored response. """ @@ -618,4 +620,150 @@ def get_responses( original_exception=e, completion_kwargs=local_vars, extra_kwargs=kwargs, - ) \ No newline at end of file + ) + + +@client +async def alist_input_items( + response_id: str, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + extra_headers: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + custom_llm_provider: Optional[str] = None, + **kwargs, +) -> Dict: + """Async: List input items for a response""" + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["alist_input_items"] = True + + decoded_response_id = ( + ResponsesAPIRequestUtils._decode_responses_api_response_id( + response_id=response_id + ) + ) + response_id = decoded_response_id.get("response_id") or response_id + custom_llm_provider = ( + decoded_response_id.get("custom_llm_provider") or custom_llm_provider + ) + + func = partial( + list_input_items, + response_id=response_id, + after=after, + before=before, + include=include, + limit=limit, + order=order, + extra_headers=extra_headers, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + return response + except Exception as e: + raise litellm.exception_type( + model=None, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def list_input_items( + response_id: str, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + extra_headers: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + custom_llm_provider: Optional[str] = None, + **kwargs, +) -> Union[Dict, Coroutine[Any, Any, Dict]]: + """List input items for a response""" + local_vars = locals() + try: + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) + _is_async = kwargs.pop("alist_input_items", False) is True + + litellm_params = GenericLiteLLMParams(**kwargs) + + decoded_response_id = ( + ResponsesAPIRequestUtils._decode_responses_api_response_id( + response_id=response_id + ) + ) + response_id = decoded_response_id.get("response_id") or response_id + custom_llm_provider = ( + decoded_response_id.get("custom_llm_provider") or custom_llm_provider + ) + + if custom_llm_provider is None: + raise ValueError("custom_llm_provider is required but passed as None") + + responses_api_provider_config: Optional[BaseResponsesAPIConfig] = ( + ProviderConfigManager.get_provider_responses_api_config( + model=None, + provider=litellm.LlmProviders(custom_llm_provider), + ) + ) + + if responses_api_provider_config is None: + raise ValueError( + f"list_input_items is not supported for {custom_llm_provider}" + ) + + local_vars.update(kwargs) + + litellm_logging_obj.update_environment_variables( + model=None, + optional_params={"response_id": response_id}, + litellm_params={"litellm_call_id": litellm_call_id}, + custom_llm_provider=custom_llm_provider, + ) + + response = base_llm_http_handler.list_responses_input_items( + response_id=response_id, + custom_llm_provider=custom_llm_provider, + responses_api_provider_config=responses_api_provider_config, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + after=after, + before=before, + include=include, + limit=limit, + order=order, + extra_headers=extra_headers, + timeout=timeout or request_timeout, + _is_async=_is_async, + client=kwargs.get("client"), + ) + + return response + except Exception as e: + raise litellm.exception_type( + model=None, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) diff --git a/litellm/router.py b/litellm/router.py index e8cba56b2495..cc71b4811057 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -349,9 +349,9 @@ def __init__( # noqa: PLR0915 ) # names of models under litellm_params. ex. azure/chatgpt-v-2 self.deployment_latency_map = {} ### CACHING ### - cache_type: Literal[ - "local", "redis", "redis-semantic", "s3", "disk" - ] = "local" # default to an in-memory cache + cache_type: Literal["local", "redis", "redis-semantic", "s3", "disk"] = ( + "local" # default to an in-memory cache + ) redis_cache = None cache_config: Dict[str, Any] = {} @@ -573,9 +573,9 @@ def __init__( # noqa: PLR0915 ) ) - self.model_group_retry_policy: Optional[ - Dict[str, RetryPolicy] - ] = model_group_retry_policy + self.model_group_retry_policy: Optional[Dict[str, RetryPolicy]] = ( + model_group_retry_policy + ) self.allowed_fails_policy: Optional[AllowedFailsPolicy] = None if allowed_fails_policy is not None: @@ -753,6 +753,9 @@ def initialize_router_endpoints(self): self.adelete_responses = self.factory_function( litellm.adelete_responses, call_type="adelete_responses" ) + self.alist_input_items = self.factory_function( + litellm.alist_input_items, call_type="alist_input_items" + ) self._arealtime = self.factory_function( litellm._arealtime, call_type="_arealtime" ) @@ -3202,6 +3205,7 @@ def factory_function( "alist_files", "aimage_edit", "allm_passthrough_route", + "alist_input_items", ] = "assistants", ): """ @@ -3262,7 +3266,11 @@ async def async_wrapper( original_function=original_function, **kwargs, ) - elif call_type in ("aget_responses", "adelete_responses"): + elif call_type in ( + "aget_responses", + "adelete_responses", + "alist_input_items", + ): return await self._init_responses_api_endpoints( original_function=original_function, **kwargs, @@ -3406,11 +3414,11 @@ async def async_function_with_fallbacks(self, *args, **kwargs): # noqa: PLR0915 if isinstance(e, litellm.ContextWindowExceededError): if context_window_fallbacks is not None: - fallback_model_group: Optional[ - List[str] - ] = self._get_fallback_model_group_from_fallbacks( - fallbacks=context_window_fallbacks, - model_group=model_group, + fallback_model_group: Optional[List[str]] = ( + self._get_fallback_model_group_from_fallbacks( + fallbacks=context_window_fallbacks, + model_group=model_group, + ) ) if fallback_model_group is None: raise original_exception @@ -3442,11 +3450,11 @@ async def async_function_with_fallbacks(self, *args, **kwargs): # noqa: PLR0915 e.message += "\n{}".format(error_message) elif isinstance(e, litellm.ContentPolicyViolationError): if content_policy_fallbacks is not None: - fallback_model_group: Optional[ - List[str] - ] = self._get_fallback_model_group_from_fallbacks( - fallbacks=content_policy_fallbacks, - model_group=model_group, + fallback_model_group: Optional[List[str]] = ( + self._get_fallback_model_group_from_fallbacks( + fallbacks=content_policy_fallbacks, + model_group=model_group, + ) ) if fallback_model_group is None: raise original_exception diff --git a/litellm/types/utils.py b/litellm/types/utils.py index e0e8f3f742f8..fed3da7b6e01 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -275,6 +275,7 @@ class CallTypes(Enum): retrieve_fine_tuning_job = "retrieve_fine_tuning_job" responses = "responses" aresponses = "aresponses" + alist_input_items = "alist_input_items" CallTypesLiteral = Literal[ diff --git a/tests/llm_responses_api_testing/base_responses_api.py b/tests/llm_responses_api_testing/base_responses_api.py index b41f5a52d821..0519b8229323 100644 --- a/tests/llm_responses_api_testing/base_responses_api.py +++ b/tests/llm_responses_api_testing/base_responses_api.py @@ -315,6 +315,32 @@ async def test_basic_openai_responses_get_endpoint(self, sync_mode): assert result.output == response.output else: raise ValueError("response is not a ResponsesAPIResponse") + + @pytest.mark.asyncio + async def test_basic_openai_list_input_items_endpoint(self): + """Test that calls the OpenAI List Input Items endpoint""" + litellm._turn_on_debug() + + response = await litellm.aresponses( + model="gpt-4o", + input="Tell me a three sentence bedtime story about a unicorn.", + ) + print("Initial response=", json.dumps(response, indent=4, default=str)) + + response_id = response.get("id") + assert response_id is not None, "Response should have an ID" + print(f"Got response_id: {response_id}") + + list_items_response = await litellm.alist_input_items( + response_id=response_id, + limit=20, + order="desc", + ) + print( + "List items response=", + json.dumps(list_items_response, indent=4, default=str), + ) + @pytest.mark.asyncio async def test_multiturn_responses_api(self): diff --git a/tests/test_litellm/llms/azure/test_azure_common_utils.py b/tests/test_litellm/llms/azure/test_azure_common_utils.py index d25c62022161..d7e9e983f99b 100644 --- a/tests/test_litellm/llms/azure/test_azure_common_utils.py +++ b/tests/test_litellm/llms/azure/test_azure_common_utils.py @@ -391,6 +391,7 @@ def test_select_azure_base_url_called(setup_mocks): "add_message", "arun_thread_stream", "aresponses", + "alist_input_items", "acreate_fine_tuning_job", "acancel_fine_tuning_job", "alist_fine_tuning_jobs", diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index 04b9de561623..0eea8da9b92a 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -1,7 +1,7 @@ import json import os import sys -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import httpx import pytest @@ -10,6 +10,8 @@ 0, os.path.abspath("../../../../..") ) # Adds the parent directory to the system path +import litellm +from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.types.llms.openai import ( OutputTextDeltaEvent, @@ -18,6 +20,7 @@ ResponsesAPIResponse, ResponsesAPIStreamEvents, ) +from litellm.types.router import GenericLiteLLMParams class TestOpenAIResponsesAPIConfig: @@ -271,3 +274,340 @@ def test_transform_streaming_response_generic_event(self): ) assert isinstance(result, GenericEvent) assert result.type == "test" + + +class TestTransformListInputItemsRequest: + """Test suite for transform_list_input_items_request function""" + + def setup_method(self): + """Setup test fixtures""" + self.openai_config = OpenAIResponsesAPIConfig() + self.azure_config = AzureOpenAIResponsesAPIConfig() + self.response_id = "resp_abc123" + self.api_base = "https://api.openai.com/v1/responses" + self.litellm_params = GenericLiteLLMParams() + self.headers = {"Authorization": "Bearer test-key"} + + def test_openai_transform_list_input_items_request_minimal(self): + """Test OpenAI implementation with minimal parameters""" + # Execute + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + ) + + # Assert + expected_url = f"{self.api_base}/{self.response_id}/input_items" + assert url == expected_url + assert params == {"limit": 20, "order": "desc"} + + def test_openai_transform_list_input_items_request_all_params(self): + """Test OpenAI implementation with all optional parameters""" + # Execute + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + after="cursor_after_123", + before="cursor_before_456", + include=["metadata", "content"], + limit=50, + order="asc", + ) + + # Assert + expected_url = f"{self.api_base}/{self.response_id}/input_items" + expected_params = { + "after": "cursor_after_123", + "before": "cursor_before_456", + "include": "metadata,content", # Should be comma-separated string + "limit": 50, + "order": "asc", + } + assert url == expected_url + assert params == expected_params + + def test_openai_transform_list_input_items_request_include_list_formatting(self): + """Test that include list is properly formatted as comma-separated string""" + # Execute + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + include=["metadata", "content", "annotations"], + ) + + # Assert + assert params["include"] == "metadata,content,annotations" + + def test_openai_transform_list_input_items_request_none_values(self): + """Test OpenAI implementation with None values for optional parameters""" + # Execute - pass only required parameters and explicit None for truly optional params + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + after=None, + before=None, + include=None, + ) + + # Assert + expected_url = f"{self.api_base}/{self.response_id}/input_items" + expected_params = { + "limit": 20, + "order": "desc", + } # Default values should be present + assert url == expected_url + assert params == expected_params + + def test_openai_transform_list_input_items_request_empty_include_list(self): + """Test OpenAI implementation with empty include list""" + # Execute + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + include=[], + ) + + # Assert + assert "include" not in params # Empty list should not be included + + def test_azure_transform_list_input_items_request_minimal(self): + """Test Azure implementation with minimal parameters""" + # Setup + azure_api_base = "https://test.openai.azure.com/openai/responses?api-version=2024-05-01-preview" + + # Execute + url, params = self.azure_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=azure_api_base, + litellm_params=self.litellm_params, + headers=self.headers, + ) + + # Assert + assert self.response_id in url + assert "/input_items" in url + assert params == {"limit": 20, "order": "desc"} + + def test_azure_transform_list_input_items_request_url_construction(self): + """Test Azure implementation URL construction with response_id in path""" + # Setup + azure_api_base = "https://test.openai.azure.com/openai/responses?api-version=2024-05-01-preview" + + # Execute + url, params = self.azure_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=azure_api_base, + litellm_params=self.litellm_params, + headers=self.headers, + ) + + # Assert + # The Azure implementation should construct URL with response_id in path + assert self.response_id in url + assert "/input_items" in url + assert "api-version=2024-05-01-preview" in url + + def test_azure_transform_list_input_items_request_with_all_params(self): + """Test Azure implementation with all optional parameters""" + # Setup + azure_api_base = "https://test.openai.azure.com/openai/responses?api-version=2024-05-01-preview" + + # Execute + url, params = self.azure_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=azure_api_base, + litellm_params=self.litellm_params, + headers=self.headers, + after="cursor_after_123", + before="cursor_before_456", + include=["metadata", "content"], + limit=100, + order="asc", + ) + + # Assert + expected_params = { + "after": "cursor_after_123", + "before": "cursor_before_456", + "include": "metadata,content", + "limit": 100, + "order": "asc", + } + assert params == expected_params + + @patch("litellm.router.Router") + def test_mock_litellm_router_with_transform_list_input_items_request( + self, mock_router + ): + """Mock test using litellm.router for transform_list_input_items_request""" + # Setup mock router + mock_router_instance = Mock() + mock_router.return_value = mock_router_instance + + # Mock the provider config + mock_provider_config = Mock(spec=OpenAIResponsesAPIConfig) + mock_provider_config.transform_list_input_items_request.return_value = ( + "https://api.openai.com/v1/responses/resp_123/input_items", + {"limit": 20, "order": "desc"}, + ) + + # Setup router mock + mock_router_instance.get_provider_responses_api_config.return_value = ( + mock_provider_config + ) + + # Test parameters + response_id = "resp_test123" + + # Execute + url, params = mock_provider_config.transform_list_input_items_request( + response_id=response_id, + api_base="https://api.openai.com/v1/responses", + litellm_params=GenericLiteLLMParams(), + headers={"Authorization": "Bearer test"}, + after="cursor_123", + include=["metadata"], + limit=30, + ) + + # Assert + mock_provider_config.transform_list_input_items_request.assert_called_once_with( + response_id=response_id, + api_base="https://api.openai.com/v1/responses", + litellm_params=GenericLiteLLMParams(), + headers={"Authorization": "Bearer test"}, + after="cursor_123", + include=["metadata"], + limit=30, + ) + assert url == "https://api.openai.com/v1/responses/resp_123/input_items" + assert params == {"limit": 20, "order": "desc"} + + @patch("litellm.list_input_items") + def test_mock_litellm_list_input_items_integration(self, mock_list_input_items): + """Test integration with litellm.list_input_items function""" + # Setup mock response + mock_response = { + "object": "list", + "data": [ + { + "id": "input_item_123", + "object": "input_item", + "type": "message", + "role": "user", + "content": "Test message", + } + ], + "has_more": False, + "first_id": "input_item_123", + "last_id": "input_item_123", + } + mock_list_input_items.return_value = mock_response + + # Execute + result = mock_list_input_items( + response_id="resp_test123", + after="cursor_after", + limit=10, + custom_llm_provider="openai", + ) + + # Assert + mock_list_input_items.assert_called_once_with( + response_id="resp_test123", + after="cursor_after", + limit=10, + custom_llm_provider="openai", + ) + assert result["object"] == "list" + assert len(result["data"]) == 1 + + def test_parameter_validation_edge_cases(self): + """Test edge cases for parameter validation""" + # Test with limit=0 + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + limit=0, + ) + assert params["limit"] == 0 + + # Test with very large limit + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + limit=1000, + ) + assert params["limit"] == 1000 + + # Test with single item in include list + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + include=["metadata"], + ) + assert params["include"] == "metadata" + + def test_url_construction_with_different_api_bases(self): + """Test URL construction with different API base formats""" + test_cases = [ + { + "api_base": "https://api.openai.com/v1/responses", + "expected_suffix": "/resp_abc123/input_items", + }, + { + "api_base": "https://api.openai.com/v1/responses/", # with trailing slash + "expected_suffix": "/resp_abc123/input_items", + }, + { + "api_base": "https://custom-api.example.com/v1/responses", + "expected_suffix": "/resp_abc123/input_items", + }, + ] + + for case in test_cases: + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=case["api_base"], + litellm_params=self.litellm_params, + headers=self.headers, + ) + assert url.endswith(case["expected_suffix"]) + + def test_return_type_validation(self): + """Test that function returns correct types""" + url, params = self.openai_config.transform_list_input_items_request( + response_id=self.response_id, + api_base=self.api_base, + litellm_params=self.litellm_params, + headers=self.headers, + ) + + # Assert return types + assert isinstance(url, str) + assert isinstance(params, dict) + + # Assert URL is properly formatted + assert url.startswith("http") + assert "input_items" in url + + # Assert params contains expected keys with correct types + for key, value in params.items(): + assert isinstance(key, str) + assert value is not None