mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
[Feat] New LLM API Endpoint - Add List input items for Responses API (#11602)
* (feat) add list_input_items * add alist_input_items to router * add GET input_items for responses API * test_basic_openai_list_input_items_endpoint * TestTransformListInputItemsRequest * test_ensure_initialize_azure_sdk_client_always_used
This commit is contained in:
parent
9058354ced
commit
4dc9626fd5
13 changed files with 875 additions and 44 deletions
|
|
@ -170,3 +170,35 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ class BaseResponsesAPIConfig(ABC):
|
|||
headers: dict,
|
||||
) -> Tuple[str, Dict]:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def transform_get_response_api_response(
|
||||
self,
|
||||
|
|
@ -165,10 +165,36 @@ class BaseResponsesAPIConfig(ABC):
|
|||
) -> 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:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import (
|
|||
Coroutine,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
|
|
@ -1812,6 +1813,168 @@ class BaseLLMHTTPHandler:
|
|||
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 @@ class BaseLLMHTTPHandler:
|
|||
_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.
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
message=raw_response.text, status_code=raw_response.status_code
|
||||
)
|
||||
return DeleteResponseResult(**raw_response_json)
|
||||
|
||||
|
||||
#########################################################
|
||||
########## GET RESPONSE API TRANSFORMATION ###############
|
||||
#########################################################
|
||||
|
|
@ -271,7 +271,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
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 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ class ProxyBaseLLMRequestProcessing:
|
|||
"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 @@ class ProxyBaseLLMRequestProcessing:
|
|||
"adelete_responses",
|
||||
"atext_completion",
|
||||
"aimage_edit",
|
||||
"alist_input_items",
|
||||
],
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
general_settings: dict,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ ROUTE_ENDPOINT_MAPPING = {
|
|||
"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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -349,9 +349,9 @@ class Router:
|
|||
) # 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 @@ class Router:
|
|||
)
|
||||
)
|
||||
|
||||
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 @@ class Router:
|
|||
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 @@ class Router:
|
|||
"alist_files",
|
||||
"aimage_edit",
|
||||
"allm_passthrough_route",
|
||||
"alist_input_items",
|
||||
] = "assistants",
|
||||
):
|
||||
"""
|
||||
|
|
@ -3262,7 +3266,11 @@ class Router:
|
|||
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 @@ class Router:
|
|||
|
||||
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 @@ class Router:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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[
|
||||
|
|
|
|||
|
|
@ -315,6 +315,32 @@ class BaseResponsesAPITest(ABC):
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 @@ sys.path.insert(
|
|||
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 @@ from litellm.types.llms.openai import (
|
|||
ResponsesAPIResponse,
|
||||
ResponsesAPIStreamEvents,
|
||||
)
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
|
||||
class TestOpenAIResponsesAPIConfig:
|
||||
|
|
@ -271,3 +274,340 @@ class TestOpenAIResponsesAPIConfig:
|
|||
)
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue