mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(proxy): return 4xx for missing required params across all LLM routes and propagate provider status on lookups
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
f586220be2
commit
f37c5e3608
10 changed files with 279 additions and 10 deletions
|
|
@ -3910,6 +3910,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=batch_response, provider_config=provider_config)
|
||||
return provider_config.transform_retrieve_batch_response(
|
||||
model=model,
|
||||
raw_response=batch_response,
|
||||
|
|
@ -4067,6 +4068,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=batch_response, provider_config=provider_config)
|
||||
return provider_config.transform_retrieve_batch_response(
|
||||
model=model,
|
||||
raw_response=batch_response,
|
||||
|
|
@ -4484,6 +4486,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=provider_config)
|
||||
return provider_config.transform_retrieve_file_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -4540,6 +4543,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=provider_config)
|
||||
return provider_config.transform_retrieve_file_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -4732,6 +4736,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=provider_config)
|
||||
files_per_page: Final = self._files_per_listing_page(
|
||||
response, provider_config, logging_obj, litellm_params, headers, sync_httpx_client, timeout
|
||||
)
|
||||
|
|
@ -4789,6 +4794,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=provider_config)
|
||||
files_per_page: Final = self._files_per_async_listing_page(
|
||||
response, provider_config, logging_obj, litellm_params, headers, async_httpx_client, timeout
|
||||
)
|
||||
|
|
@ -5925,6 +5931,38 @@ class BaseLLMHTTPHandler:
|
|||
|
||||
return None
|
||||
|
||||
def _raise_for_provider_error_status(
|
||||
self,
|
||||
response: httpx.Response,
|
||||
provider_config: Union[
|
||||
BaseConfig,
|
||||
BaseRerankConfig,
|
||||
BaseResponsesAPIConfig,
|
||||
BaseImageEditConfig,
|
||||
BaseImageGenerationConfig,
|
||||
BaseVectorStoreConfig,
|
||||
BaseVectorStoreFilesConfig,
|
||||
BaseGoogleGenAIGenerateContentConfig,
|
||||
BaseAnthropicMessagesConfig,
|
||||
BaseBatchesConfig,
|
||||
BaseVideoConfig,
|
||||
BaseSearchConfig,
|
||||
BaseTextToSpeechConfig,
|
||||
BaseSkillsAPIConfig,
|
||||
"BasePassthroughConfig",
|
||||
"BaseContainerConfig",
|
||||
BaseEvalsAPIConfig,
|
||||
BaseRealtimeHTTPConfig,
|
||||
],
|
||||
) -> None:
|
||||
if not httpx.codes.is_error(response.status_code):
|
||||
return
|
||||
raise provider_config.get_error_class(
|
||||
error_message=response.text,
|
||||
status_code=response.status_code,
|
||||
headers=response.headers,
|
||||
)
|
||||
|
||||
def _handle_error(
|
||||
self,
|
||||
e: Exception,
|
||||
|
|
@ -7337,6 +7375,7 @@ class BaseLLMHTTPHandler:
|
|||
)
|
||||
|
||||
# Transform the response using the provider config
|
||||
self._raise_for_provider_error_status(response=response, provider_config=video_content_provider_config)
|
||||
return video_content_provider_config.transform_video_content_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -7415,6 +7454,7 @@ class BaseLLMHTTPHandler:
|
|||
)
|
||||
|
||||
# Transform the response using the provider config
|
||||
self._raise_for_provider_error_status(response=response, provider_config=video_content_provider_config)
|
||||
return await video_content_provider_config.async_transform_video_content_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -8388,6 +8428,7 @@ class BaseLLMHTTPHandler:
|
|||
params=params,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=video_list_provider_config)
|
||||
return video_list_provider_config.transform_video_list_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -8569,6 +8610,7 @@ class BaseLLMHTTPHandler:
|
|||
headers=headers,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=video_status_provider_config)
|
||||
return video_status_provider_config.transform_video_status_retrieve_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -8659,6 +8701,7 @@ class BaseLLMHTTPHandler:
|
|||
url=url,
|
||||
headers=headers,
|
||||
)
|
||||
self._raise_for_provider_error_status(response=response, provider_config=video_status_provider_config)
|
||||
return await video_status_provider_config.async_transform_video_status_retrieve_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -10156,6 +10199,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_provider_config)
|
||||
return vector_store_provider_config.transform_create_vector_store_response(
|
||||
response=response,
|
||||
)
|
||||
|
|
@ -10220,6 +10264,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_provider_config)
|
||||
return vector_store_provider_config.transform_create_vector_store_response(
|
||||
response=response,
|
||||
)
|
||||
|
|
@ -10286,6 +10331,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_provider_config)
|
||||
return response.json()
|
||||
|
||||
def vector_store_list_handler(
|
||||
|
|
@ -10364,6 +10410,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_provider_config)
|
||||
return response.json()
|
||||
|
||||
async def async_vector_store_update_handler(
|
||||
|
|
@ -10832,6 +10879,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_files_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_files_provider_config)
|
||||
return vector_store_files_provider_config.transform_list_vector_store_files_response(response=response)
|
||||
|
||||
def vector_store_file_list_handler(
|
||||
|
|
@ -10908,6 +10956,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_files_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_files_provider_config)
|
||||
return vector_store_files_provider_config.transform_list_vector_store_files_response(response=response)
|
||||
|
||||
async def async_vector_store_file_retrieve_handler(
|
||||
|
|
@ -10967,6 +11016,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_files_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_files_provider_config)
|
||||
return vector_store_files_provider_config.transform_retrieve_vector_store_file_response(response=response)
|
||||
|
||||
def vector_store_file_retrieve_handler(
|
||||
|
|
@ -11037,6 +11087,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_files_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_files_provider_config)
|
||||
return vector_store_files_provider_config.transform_retrieve_vector_store_file_response(response=response)
|
||||
|
||||
async def async_vector_store_file_content_handler(
|
||||
|
|
@ -11096,6 +11147,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_files_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_files_provider_config)
|
||||
return vector_store_files_provider_config.transform_retrieve_vector_store_file_content_response(
|
||||
response=response
|
||||
)
|
||||
|
|
@ -11168,6 +11220,7 @@ class BaseLLMHTTPHandler:
|
|||
except Exception as e:
|
||||
raise self._handle_error(e=e, provider_config=vector_store_files_provider_config)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=vector_store_files_provider_config)
|
||||
return vector_store_files_provider_config.transform_retrieve_vector_store_file_content_response(
|
||||
response=response
|
||||
)
|
||||
|
|
@ -12128,6 +12181,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=skills_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=skills_api_provider_config)
|
||||
return skills_api_provider_config.transform_list_skills_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12175,6 +12229,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=skills_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=skills_api_provider_config)
|
||||
return skills_api_provider_config.transform_list_skills_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12231,6 +12286,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=skills_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=skills_api_provider_config)
|
||||
return skills_api_provider_config.transform_get_skill_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12276,6 +12332,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=skills_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=skills_api_provider_config)
|
||||
return skills_api_provider_config.transform_get_skill_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12546,6 +12603,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_list_evals_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12593,6 +12651,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_list_evals_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12649,6 +12708,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_get_eval_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -12694,6 +12754,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_get_eval_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -13171,6 +13232,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_list_runs_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -13218,6 +13280,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_list_runs_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -13274,6 +13337,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_get_run_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -13319,6 +13383,7 @@ class BaseLLMHTTPHandler:
|
|||
provider_config=evals_api_provider_config,
|
||||
)
|
||||
|
||||
self._raise_for_provider_error_status(response=response, provider_config=evals_api_provider_config)
|
||||
return evals_api_provider_config.transform_get_run_response(
|
||||
raw_response=response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
|
|||
|
|
@ -12172,6 +12172,8 @@ async def completion(
|
|||
)
|
||||
litellm_call_id: Final = request_litellm_call_id(data)
|
||||
log_llm_api_exception(e, litellm_call_id)
|
||||
if isinstance(e, ProxyException):
|
||||
raise with_litellm_call_id(e, litellm_call_id)
|
||||
error_msg: Final = f"{e}"
|
||||
raise ProxyException(
|
||||
message=getattr(e, "message", error_msg),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
|
||||
import httpx
|
||||
|
|
@ -165,8 +166,35 @@ REQUIRED_BODY_PARAMS_BY_ROUTE: Final[Mapping[str, tuple[str, ...]]] = {
|
|||
"amoderation": ("input",),
|
||||
"aimage_generation": ("prompt",),
|
||||
"asearch": ("query",),
|
||||
"atext_completion": ("prompt",),
|
||||
"atranscription": ("file",),
|
||||
"arerank": ("query", "documents"),
|
||||
"acompact_responses": ("input",),
|
||||
"aimage_edit": ("image", "prompt"),
|
||||
"anthropic_messages": ("messages", "max_tokens"),
|
||||
"agenerate_content": ("contents",),
|
||||
"aocr": ("document",),
|
||||
"acreate_fine_tuning_job": ("training_file",),
|
||||
"avector_store_search": ("query",),
|
||||
"avector_store_file_create": ("file_id",),
|
||||
"avector_store_file_update": ("attributes",),
|
||||
"avideo_generation": ("prompt",),
|
||||
"avideo_remix": ("prompt",),
|
||||
"avideo_edit": ("prompt",),
|
||||
"avideo_extension": ("prompt", "seconds"),
|
||||
"avideo_create_character": ("name", "video"),
|
||||
"acreate_container": ("name",),
|
||||
"aupload_container_file": ("file",),
|
||||
"acreate_agent": ("name",),
|
||||
"acreate_interaction": ("input",),
|
||||
"acreate_eval": ("data_source_config", "testing_criteria"),
|
||||
"acreate_run": ("data_source",),
|
||||
}
|
||||
|
||||
REQUIRED_ONE_OF_BODY_PARAMS_BY_ROUTE: Final[Mapping[str, tuple[str, str]]] = MappingProxyType(
|
||||
{"acreate_interaction": ("model", "agent")}
|
||||
)
|
||||
|
||||
|
||||
class ProxyMissingRequiredParamError(ProxyException):
|
||||
def __init__(self, route: str, param: str):
|
||||
|
|
@ -178,11 +206,18 @@ class ProxyMissingRequiredParamError(ProxyException):
|
|||
)
|
||||
|
||||
|
||||
def raise_if_required_body_param_missing(route_type: str, data: Mapping[str, object]) -> None:
|
||||
missing_param: Final = next(
|
||||
def _find_missing_required_body_param(route_type: str, data: Mapping[str, object]) -> str | None:
|
||||
one_of_params: Final = REQUIRED_ONE_OF_BODY_PARAMS_BY_ROUTE.get(route_type)
|
||||
if one_of_params is not None and all(data.get(param) is None for param in one_of_params):
|
||||
return one_of_params[0]
|
||||
return next(
|
||||
(param for param in REQUIRED_BODY_PARAMS_BY_ROUTE.get(route_type, ()) if data.get(param) is None),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def raise_if_required_body_param_missing(route_type: str, data: Mapping[str, object]) -> None:
|
||||
missing_param: Final = _find_missing_required_body_param(route_type, data)
|
||||
if missing_param is None:
|
||||
return
|
||||
raise ProxyMissingRequiredParamError(
|
||||
|
|
@ -634,6 +669,11 @@ async def _route_request_single_attempt( # noqa: ANN202 # returns unawaited pr
|
|||
# These endpoints don't need a model, use custom_llm_provider directly
|
||||
return getattr(litellm, f"{route_type}")(**data)
|
||||
|
||||
if "model" not in data:
|
||||
raise ProxyMissingRequiredParamError(
|
||||
route=ROUTE_ENDPOINT_MAPPING.get(route_type, route_type),
|
||||
param="model",
|
||||
)
|
||||
team_model_name: Final = llm_router.map_team_model(data["model"], team_id) if team_id is not None else None
|
||||
if team_model_name is not None:
|
||||
data["model"] = team_model_name
|
||||
|
|
|
|||
|
|
@ -369,6 +369,11 @@ async def list_vector_stores(
|
|||
- page: int - Page number for pagination (default: 1)
|
||||
- page_size: int - Number of items per page (default: 100)
|
||||
"""
|
||||
if page < 1 or page_size < 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"page and page_size must be >= 1, got page={page}, page_size={page_size}",
|
||||
)
|
||||
await check_feature_access_for_user(user_api_key_dict, "vector_stores")
|
||||
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
|
|
|||
|
|
@ -1188,7 +1188,7 @@ def function_setup(
|
|||
elif call_type == CallTypes.moderation.value or call_type == CallTypes.amoderation.value:
|
||||
messages = args[1] if len(args) > 1 else kwargs["input"]
|
||||
elif call_type == CallTypes.atext_completion.value or call_type == CallTypes.text_completion.value:
|
||||
messages = args[0] if len(args) > 0 else kwargs["prompt"]
|
||||
messages = args[0] if len(args) > 0 else kwargs.get("prompt")
|
||||
elif call_type == CallTypes.rerank.value or call_type == CallTypes.arerank.value:
|
||||
messages = kwargs.get("query")
|
||||
elif call_type in (CallTypes.search.value, CallTypes.asearch.value):
|
||||
|
|
|
|||
|
|
@ -601,6 +601,13 @@ def test_fallback_login_has_no_deprecation_banner(client_no_auth):
|
|||
assert "<form" in html
|
||||
|
||||
|
||||
def test_text_completion_without_a_prompt_returns_400_naming_prompt(client_no_auth):
|
||||
response = client_no_auth.post("/v1/completions", json={"model": "vllm_embed_model"})
|
||||
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json()["error"]["param"] == "prompt", response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ui_logo_path",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ from litellm.proxy.route_llm_request import ProxyModelNotFoundError, route_reque
|
|||
@pytest.mark.parametrize(
|
||||
"route_type, required_body_params",
|
||||
[
|
||||
("atext_completion", {}),
|
||||
("atext_completion", {"prompt": "Hello"}),
|
||||
("acompletion", {"messages": [{"role": "user", "content": "Hello"}]}),
|
||||
("aembedding", {"input": "Hello"}),
|
||||
("aimage_generation", {"prompt": "a cat"}),
|
||||
("aspeech", {"input": "Hello"}),
|
||||
("atranscription", {}),
|
||||
("atranscription", {"file": b"audio"}),
|
||||
("amoderation", {"input": "Hello"}),
|
||||
("arerank", {}),
|
||||
("arerank", {"query": "Hello", "documents": ["hi"]}),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -251,7 +251,7 @@ async def test_route_request_no_model_required():
|
|||
|
||||
for route_type in test_cases:
|
||||
# Test data without model parameter
|
||||
data = {"input": "test input", "api_key": "test-key"}
|
||||
data = {"input": "test input", "query": "test query", "api_key": "test-key"}
|
||||
|
||||
llm_router = MagicMock()
|
||||
getattr(llm_router, route_type).return_value = "fake_response"
|
||||
|
|
@ -282,6 +282,7 @@ async def test_route_request_no_model_required_with_router_settings():
|
|||
# Test data with model parameter (it will be ignored for these route types)
|
||||
data = {
|
||||
"input": "test input",
|
||||
"query": "test query",
|
||||
"model": "test-model", # Include dummy model to avoid KeyError
|
||||
}
|
||||
|
||||
|
|
@ -1047,10 +1048,47 @@ async def test_route_request_override_enable_tag_filtering_beats_body_value():
|
|||
("amoderation", "input", "/moderations"),
|
||||
("aimage_generation", "prompt", "/image/generations"),
|
||||
("asearch", "query", "/search"),
|
||||
("atext_completion", "prompt", "/completions"),
|
||||
("atranscription", "file", "/audio/transcriptions"),
|
||||
("arerank", "query", "/rerank"),
|
||||
("acompact_responses", "input", "/responses/compact"),
|
||||
("aimage_edit", "image", "/images/edits"),
|
||||
("anthropic_messages", "messages", "anthropic_messages"),
|
||||
("agenerate_content", "contents", "agenerate_content"),
|
||||
("aocr", "document", "/ocr"),
|
||||
("acreate_fine_tuning_job", "training_file", "acreate_fine_tuning_job"),
|
||||
("avector_store_search", "query", "avector_store_search"),
|
||||
("avector_store_file_create", "file_id", "avector_store_file_create"),
|
||||
("avector_store_file_update", "attributes", "avector_store_file_update"),
|
||||
("avideo_generation", "prompt", "/videos"),
|
||||
("avideo_remix", "prompt", "/videos/{video_id}/remix"),
|
||||
("avideo_edit", "prompt", "/videos/edits"),
|
||||
("avideo_extension", "prompt", "/videos/extensions"),
|
||||
("avideo_create_character", "name", "/videos/characters"),
|
||||
("acreate_container", "name", "/containers"),
|
||||
("aupload_container_file", "file", "/containers/{container_id}/files"),
|
||||
("acreate_agent", "name", "/v1beta/agents"),
|
||||
("acreate_eval", "data_source_config", "/evals"),
|
||||
("acreate_run", "data_source", "/evals/{eval_id}/runs"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"data_extra", [{}, {"messages": None, "input": None, "input_file_id": None, "prompt": None, "query": None}]
|
||||
"data_extra",
|
||||
[
|
||||
{},
|
||||
{
|
||||
"messages": None,
|
||||
"input": None,
|
||||
"input_file_id": None,
|
||||
"prompt": None,
|
||||
"query": None,
|
||||
"file": None,
|
||||
"image": None,
|
||||
"contents": None,
|
||||
"document": None,
|
||||
"name": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_raise_if_required_body_param_missing_rejects_missing_param(route_type, param, route, data_extra):
|
||||
from litellm.proxy.route_llm_request import (
|
||||
|
|
@ -1088,16 +1126,49 @@ def test_raise_if_required_body_param_missing_names_first_missing_batch_param(da
|
|||
assert exc_info.value.param == param
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route_type, data, param",
|
||||
[
|
||||
("arerank", {"model": "rerank-model", "query": "hi"}, "documents"),
|
||||
("aimage_edit", {"model": "gpt-image-1", "image": b"png"}, "prompt"),
|
||||
("anthropic_messages", {"model": "claude", "messages": []}, "max_tokens"),
|
||||
("avideo_extension", {"model": "sora-2", "prompt": "longer"}, "seconds"),
|
||||
("avideo_create_character", {"name": "hero"}, "video"),
|
||||
("acreate_eval", {"data_source_config": {"type": "custom"}}, "testing_criteria"),
|
||||
("acreate_interaction", {"input": "hi"}, "model"),
|
||||
("acreate_interaction", {"model": None, "agent": None, "input": "hi"}, "model"),
|
||||
("acreate_interaction", {"model": "gemini-3-pro-preview"}, "input"),
|
||||
],
|
||||
)
|
||||
def test_raise_if_required_body_param_missing_names_each_missing_param(route_type, data, param):
|
||||
from litellm.proxy.route_llm_request import (
|
||||
ProxyMissingRequiredParamError,
|
||||
raise_if_required_body_param_missing,
|
||||
)
|
||||
|
||||
with pytest.raises(ProxyMissingRequiredParamError) as exc_info:
|
||||
raise_if_required_body_param_missing(route_type=route_type, data=data)
|
||||
|
||||
assert exc_info.value.code == "400"
|
||||
assert exc_info.value.param == param
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route_type, data",
|
||||
[
|
||||
("acompletion", {"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]}),
|
||||
("acompletion", {"model": "gpt-4o", "messages": []}),
|
||||
("atext_completion", {"model": "gpt-4o"}),
|
||||
("atext_completion", {"model": "gpt-4o", "prompt": "hi"}),
|
||||
("aembedding", {"model": "text-embedding-3-small", "input": "hi"}),
|
||||
("aresponses", {"model": "gpt-4o", "input": "hi"}),
|
||||
("aresponses", {"model": "gpt-4o", "input": []}),
|
||||
("arerank", {"model": "rerank-model"}),
|
||||
("arerank", {"model": "rerank-model", "query": "hi", "documents": ["hello"]}),
|
||||
("aimage_edit", {"model": "gpt-image-1", "image": b"png", "prompt": "a hat"}),
|
||||
("anthropic_messages", {"model": "claude", "messages": [], "max_tokens": 16}),
|
||||
("avideo_extension", {"model": "sora-2", "prompt": "longer", "seconds": "4"}),
|
||||
("acreate_eval", {"data_source_config": {"type": "custom"}, "testing_criteria": []}),
|
||||
("acreate_interaction", {"model": "gemini-3-pro-preview", "input": "hi"}),
|
||||
("acreate_interaction", {"agent": "deep-research", "input": "hi"}),
|
||||
("aimage_generation", {"model": "gpt-image-1", "prompt": "a cat"}),
|
||||
("aspeech", {"model": "gpt-4o-mini-tts", "input": "hi", "voice": "alloy"}),
|
||||
("amoderation", {"model": "omni-moderation-latest", "input": ""}),
|
||||
|
|
@ -1333,3 +1404,23 @@ def test_proxy_model_not_found_error_keeps_the_raw_model_only_in_the_client_resp
|
|||
assert raw_model in error.detail["error"]
|
||||
assert raw_model not in error.spend_log_error_message
|
||||
assert error.spend_log_error_message.startswith("/chat/completions: Invalid model name passed in")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_route_request_without_model_on_model_routed_endpoint_is_a_400():
|
||||
import litellm
|
||||
from litellm.proxy.route_llm_request import ProxyMissingRequiredParamError
|
||||
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{"model_name": "rerank-model", "litellm_params": {"model": "cohere/rerank-v3.5", "api_key": "fake"}}
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(ProxyMissingRequiredParamError) as exc_info:
|
||||
await route_request(
|
||||
data={"query": "hi", "documents": ["hello"]}, llm_router=router, user_model=None, route_type="arerank"
|
||||
)
|
||||
|
||||
assert exc_info.value.code == "400"
|
||||
assert exc_info.value.param == "model"
|
||||
|
|
|
|||
|
|
@ -135,3 +135,17 @@ async def test_list_vector_stores_admin_not_blocked():
|
|||
):
|
||||
# Must not raise any HTTPException — admin is always allowed.
|
||||
await list_vector_stores(user_api_key_dict=admin)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("page, page_size", [(0, 10), (-1, 10), (1, 0), (1, -5)])
|
||||
async def test_list_vector_stores_rejects_non_positive_pagination_with_400(page, page_size):
|
||||
from litellm.proxy.vector_store_endpoints.management_endpoints import (
|
||||
list_vector_stores,
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await list_vector_stores(user_api_key_dict=_make_internal_user(), page=page, page_size=page_size)
|
||||
|
||||
assert exc_info.value.status_code == 400, exc_info.value.detail
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ from litellm.llms.azure.videos.transformation import AzureVideoConfig
|
|||
from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import (
|
||||
AmazonAnthropicClaudeMessagesConfig,
|
||||
)
|
||||
from litellm.llms.anthropic.skills.transformation import AnthropicSkillsConfig
|
||||
from litellm.llms.openai.evals.transformation import OpenAIEvalsConfig
|
||||
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
|
||||
from litellm.llms.tinyfish.search.transformation import TinyfishSearchConfig
|
||||
from litellm.types.llms.openai import HttpxBinaryResponseContent, ResponsesAPIResponse
|
||||
|
|
@ -4302,3 +4304,41 @@ async def test_async_text_to_speech_handler_records_upstream_response_headers():
|
|||
|
||||
assert response.content == b"audio-bytes"
|
||||
_assert_upstream_headers_recorded(response)
|
||||
|
||||
|
||||
|
||||
async def _get_by_id_with_upstream(handler_name: str, upstream_response: httpx.Response) -> object:
|
||||
async_client: Final = AsyncHTTPHandler()
|
||||
await async_client.close()
|
||||
async_client.client = httpx.AsyncClient(transport=httpx.MockTransport(lambda request: upstream_response))
|
||||
handler: Final = BaseLLMHTTPHandler()
|
||||
if handler_name == "get_eval":
|
||||
return await handler.async_get_eval_handler(
|
||||
url="https://api.example.test/v1/evals/eval_missing",
|
||||
evals_api_provider_config=OpenAIEvalsConfig(),
|
||||
custom_llm_provider="openai",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
logging_obj=Mock(),
|
||||
client=async_client,
|
||||
)
|
||||
return await handler.async_get_skill_handler(
|
||||
url="https://api.example.test/v1/skills/skill_missing",
|
||||
skills_api_provider_config=AnthropicSkillsConfig(),
|
||||
custom_llm_provider="anthropic",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
logging_obj=Mock(),
|
||||
client=async_client,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("handler_name", ("get_eval", "get_skill"))
|
||||
@pytest.mark.parametrize("status_code", (400, 401, 404, 429, 503))
|
||||
async def test_get_by_id_handlers_raise_the_provider_error_status(handler_name: str, status_code: int) -> None:
|
||||
upstream_response: Final = httpx.Response(status_code, json={"error": {"message": "No such object"}})
|
||||
|
||||
with pytest.raises(BaseLLMException) as error:
|
||||
await _get_by_id_with_upstream(handler_name, upstream_response)
|
||||
|
||||
assert error.value.status_code == status_code
|
||||
assert "No such object" in error.value.message
|
||||
|
|
|
|||
|
|
@ -6437,6 +6437,11 @@ def test_function_setup_logs_the_search_query_edit_prompt_and_ocr_document_summa
|
|||
assert _logged_request_messages(original_function, *args, **kwargs) == [{"role": "user", "content": expected}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("original_function", ("atext_completion", "text_completion"))
|
||||
def test_function_setup_without_a_prompt_leaves_the_missing_prompt_to_request_validation(original_function: str) -> None:
|
||||
assert _logged_request_messages(original_function, model="gpt-4o") is None
|
||||
|
||||
|
||||
def test_search_with_a_mixed_type_query_list_still_reaches_its_own_validation_error() -> None:
|
||||
mixed_query: Final = cast(list[str], ["Eiffel Tower", 7]) # cast-ok: the invalid list is the point of the test
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue