Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions litellm/proxy/route_llm_request.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re
from typing import TYPE_CHECKING, Any, Literal, Optional

from fastapi import HTTPException, status

import litellm
from litellm.exceptions import BadRequestError

if TYPE_CHECKING:
from litellm.router import Router as _Router
Expand Down Expand Up @@ -131,6 +133,40 @@ async def route_request(
"""
Common helper to route the request
"""
try:
return await _route_request_impl(data, llm_router, user_model, route_type)
except TypeError as e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is already fixed in utils.py why have it again here?

Copy link
Contributor Author

@hula-la hula-la Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krrishdholakia
Due to the code below, certain requests(ex. aresponses), wrap client method (ex. litellm.aresponses), so the @client logic is executed.

# router.py
        self.aresponses = self.factory_function(
            litellm.aresponses, call_type="aresponses"
        )

In contrast, requests like acompletion go directly to methods implemented as shown below, so they do not go through the validation performed by the @client wrapping.

# router.py
    async def acompletion(
        self,
        model: str,
        messages: List[AllMessageValues],
        stream: bool = False,
        **kwargs,
    ):
        try:
         ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krrishdholakia I’m wondering if this change could cause any issues.

error_msg = str(e)
if "missing" in error_msg.lower() or "required" in error_msg.lower():
# Extract the missing parameter name from the error message
missing_param_match = re.search(r"missing (\d+ )?required positional argument[s]?:? '?([^']+)'?", error_msg)
if missing_param_match:
missing_param = missing_param_match.group(2)
raise BadRequestError(
message=f"Missing required parameter: {missing_param}",
model=data.get("model", ""),
llm_provider="",
)
else:
raise BadRequestError(
message="Missing required parameters",
model=data.get("model", ""),
llm_provider="",
)
else:
# Re-raise other TypeError exceptions
raise e


async def _route_request_impl(
data: dict,
llm_router: Optional[LitellmRouter],
user_model: Optional[str],
route_type: str,
):
"""
Internal implementation of route_request
"""
add_shared_session_to_data(data)

team_id = get_team_id_from_data(data)
Expand Down
56 changes: 53 additions & 3 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ def function_setup( # noqa: PLR0915
call_type == CallTypes.aresponses.value
or call_type == CallTypes.responses.value
):
messages = args[0] if len(args) > 0 else kwargs["input"]
messages = args[0] if len(args) > 0 else kwargs.get("input", None)
else:
messages = "default-message-value"
stream = False
Expand Down Expand Up @@ -1247,7 +1247,32 @@ def wrapper(*args, **kwargs): # noqa: PLR0915
except Exception as e:
print_verbose(f"Error while checking max token limit: {str(e)}")
# MODEL CALL
result = original_function(*args, **kwargs)
try:
result = original_function(*args, **kwargs)
except TypeError as e:
error_msg = str(e)
if "missing" in error_msg.lower() or "required" in error_msg.lower():
# Extract the missing parameter name from the error message
import re
missing_param_match = re.search(r"missing (\d+ )?required positional argument[s]?:? '?([^']+)'?", error_msg)
if missing_param_match:
missing_param = missing_param_match.group(2)
from litellm.exceptions import BadRequestError
raise BadRequestError(
message=f"Missing required parameter: {missing_param}",
model=model,
llm_provider="",
)
else:
from litellm.exceptions import BadRequestError
raise BadRequestError(
message="Missing required parameters",
model=model,
llm_provider="",
)
else:
# Re-raise other TypeError exceptions
raise e
end_time = datetime.datetime.now()
if _is_streaming_request(
kwargs=kwargs,
Expand Down Expand Up @@ -1485,7 +1510,32 @@ async def wrapper_async(*args, **kwargs): # noqa: PLR0915
print_verbose(f"Error while checking max token limit: {str(e)}")

# MODEL CALL
result = await original_function(*args, **kwargs)
try:
result = await original_function(*args, **kwargs)
except TypeError as e:
error_msg = str(e)
if "missing" in error_msg.lower() or "required" in error_msg.lower():
# Extract the missing parameter name from the error message
import re
missing_param_match = re.search(r"missing (\d+ )?required positional argument[s]?:? '?([^']+)'?", error_msg)
if missing_param_match:
missing_param = missing_param_match.group(2)
from litellm.exceptions import BadRequestError
raise BadRequestError(
message=f"Missing required parameter: {missing_param}",
model=model,
llm_provider="",
)
else:
from litellm.exceptions import BadRequestError
raise BadRequestError(
message="Missing required parameters",
model=model,
llm_provider="",
)
else:
# Re-raise other TypeError exceptions
raise e
end_time = datetime.datetime.now()
if _is_streaming_request(
kwargs=kwargs,
Expand Down
223 changes: 223 additions & 0 deletions tests/proxy_unit_tests/test_proxy_parameter_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""
Test parameter validation error handling through proxy server.

This test verifies that TypeError exceptions from missing required parameters
are properly converted to BadRequestError with HTTP 400 status code when
requests are made through the proxy server (route_request).
"""
import json
import os
import sys
from unittest import mock

from dotenv import load_dotenv

load_dotenv()
import asyncio
import pytest
from fastapi.testclient import TestClient

sys.path.insert(
0, os.path.abspath("../..")
)

import litellm
from litellm.proxy.proxy_server import initialize, router, app


@pytest.fixture
def client():
"""Initialize proxy server and return test client."""
# Use a basic config without any special authentication
filepath = os.path.dirname(os.path.abspath(__file__))
config_fp = f"{filepath}/test_configs/test_config_no_auth.yaml"

# If config doesn't exist, initialize without config
if os.path.exists(config_fp):
asyncio.run(initialize(config=config_fp))
else:
asyncio.run(initialize())

return TestClient(app)


def test_proxy_chat_completion_missing_model(client):
"""
Test that missing 'model' parameter returns HTTP 400 through proxy.

When a request is made through the proxy server without the required
'model' parameter, it should return HTTP 400 Bad Request.
"""
test_data = {
"messages": [
{"role": "user", "content": "test"},
],
# model parameter is intentionally omitted
}

response = client.post("/chat/completions", json=test_data)

# Verify HTTP 400 status code
assert response.status_code == 400, f"Expected status code 400, got {response.status_code}"

# Verify error response format
json_response = response.json()
assert "error" in json_response, "Response should contain 'error' key"

error = json_response["error"]
assert "message" in error, "Error should contain 'message' key"

error_message = error["message"].lower()
# Check that error message mentions missing or required parameter
assert "missing" in error_message or "required" in error_message or "model" in error_message, \
f"Error message should mention missing/required parameter, got: {error['message']}"


def test_proxy_chat_completion_missing_messages(client):
"""
Test that missing 'messages' parameter returns HTTP 400 through proxy.

Note: In current implementation, 'messages' has a default value of [],
so this might not raise an error. This test documents the behavior.
"""
test_data = {
"model": "gpt-3.5-turbo",
# messages parameter is intentionally omitted
}

response = client.post("/chat/completions", json=test_data)

# Since messages has default value [], the request might succeed or fail
# depending on the provider. We just verify the response is valid.
assert response.status_code in [200, 400, 401, 500], \
f"Response should be a valid HTTP status code, got {response.status_code}"


def test_proxy_acompletion_missing_model(client):
"""
Test that missing 'model' parameter in async completion returns HTTP 400 through proxy.

When an async completion request is made through the proxy server without
the required 'model' parameter, it should return HTTP 400 Bad Request.

This test verifies that route_request properly handles parameter validation
for async endpoints (acompletion).
"""
test_data = {
"messages": [
{"role": "user", "content": "test"},
],
# model parameter is intentionally omitted
}

response = client.post("/chat/completions", json=test_data)

# Verify HTTP 400 status code
assert response.status_code == 400, f"Expected status code 400, got {response.status_code}"

# Verify error response format
json_response = response.json()
assert "error" in json_response, "Response should contain 'error' key"

error = json_response["error"]
assert "message" in error, "Error should contain 'message' key"

error_message = error["message"].lower()
# Check that error message mentions missing or required parameter
assert "missing" in error_message or "required" in error_message or "model" in error_message, \
f"Error message should mention missing/required parameter, got: {error['message']}"


@pytest.mark.asyncio
async def test_proxy_responses_api_missing_model(client):
"""
Test that missing 'model' parameter in responses API returns HTTP 400.

The responses API requires both 'model' and 'input' parameters.
"""
test_data = {
"input": [
{
"role": "user",
"content": [{"type": "input_text", "text": "test"}],
"type": "message"
}
],
# model parameter is intentionally omitted
}

response = client.post("/v1/responses", json=test_data)

# Verify HTTP 400 status code
assert response.status_code == 400, f"Expected status code 400, got {response.status_code}"

# Verify error response format
json_response = response.json()
assert "error" in json_response, "Response should contain 'error' key"

error = json_response["error"]
assert "message" in error, "Error should contain 'message' key"


@pytest.mark.asyncio
async def test_proxy_responses_api_wrong_parameter(client):
"""
Test that using 'messages' instead of 'input' for responses API returns HTTP 400.

Responses API expects 'input' parameter, not 'messages'.
Using wrong parameter should return a clear error.
"""
test_data = {
"model": "test_openai_models",
"messages": [{"role": "user", "content": "test"}], # Wrong: should be 'input'
}

response = client.post("/v1/responses", json=test_data)

# Should return 400 for wrong parameter
assert response.status_code == 400, f"Expected status code 400, got {response.status_code}"

# Verify error response format
json_response = response.json()
assert "error" in json_response, "Response should contain 'error' key"


def test_proxy_error_response_format(client):
"""
Test that error responses follow OpenAI error format.

Error responses should have the structure:
{
"error": {
"message": str,
"type": str (optional),
"param": str (optional),
"code": str
}
}
"""
# Use model that exists in test config (test_openai_models)
# Pass wrong parameter type to trigger parameter validation error
test_data = {
"input": [{"role": "user", "content": "test"}], # Wrong: should be 'messages' for chat/completions
"model": "test_openai_models",
}

response = client.post("/chat/completions", json=test_data)

assert response.status_code == 400

json_response = response.json()
assert "error" in json_response

error = json_response["error"]

# Verify required fields
assert "message" in error, "Error must have 'message' field"
assert isinstance(error["message"], str), "Error message must be string"

# Verify code field if present
if "code" in error:
# OpenAI SDK requires code to be string
# https://github.com/openai/openai-python/blob/main/src/openai/types/shared/error_object.py
assert isinstance(error["code"], str), "Error code must be string"
Loading
Loading