[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:
Ishaan Jaff 2025-06-10 15:47:16 -07:00 • committed by GitHub
parent 9058354ced
commit 4dc9626fd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 875 additions and 44 deletions

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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
)

View file

@ -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,

View file

@ -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,
)

View file

@ -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)

View file

@ -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,
)

View file

@ -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

View file

@ -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[

View file

@ -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):

View file

@ -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",

View file

@ -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