mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-20 06:23:46 +00:00
merge: resolve conflicts between main and litellm_oss_staging_03_11_2026
This commit is contained in:
@@ -1,142 +0,0 @@
|
||||
"""
|
||||
Test that the OpenAPI schema generated by FastAPI is valid for specific endpoints.
|
||||
|
||||
Validates fixes for:
|
||||
- /spend/calculate response schema (must use proper OpenAPI 3.x content wrapper)
|
||||
- /credentials/by_model/{model_id} path parameter (must not leak credential_name)
|
||||
|
||||
Related issue: https://github.com/BerriAI/litellm/issues/21305
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestSpendCalculateOpenAPISchema:
|
||||
"""Test /spend/calculate response schema is valid OpenAPI 3.x."""
|
||||
|
||||
def test_response_schema_has_description(self):
|
||||
"""The 200 response must have a 'description' field per OpenAPI 3.x spec."""
|
||||
from litellm.proxy.spend_tracking.spend_management_endpoints import router
|
||||
|
||||
for route in router.routes:
|
||||
if hasattr(route, "path") and route.path == "/spend/calculate":
|
||||
responses = route.responses or {}
|
||||
response_200 = responses.get(200, {})
|
||||
assert "description" in response_200, (
|
||||
"/spend/calculate 200 response must have a 'description' field"
|
||||
)
|
||||
break
|
||||
else:
|
||||
pytest.fail("/spend/calculate route not found in router")
|
||||
|
||||
def test_response_schema_has_content_wrapper(self):
|
||||
"""The 200 response must use 'content' wrapper, not bare properties."""
|
||||
from litellm.proxy.spend_tracking.spend_management_endpoints import router
|
||||
|
||||
for route in router.routes:
|
||||
if hasattr(route, "path") and route.path == "/spend/calculate":
|
||||
responses = route.responses or {}
|
||||
response_200 = responses.get(200, {})
|
||||
# Must NOT have 'cost' as a top-level key (invalid OpenAPI)
|
||||
assert "cost" not in response_200, (
|
||||
"/spend/calculate 200 response must not have 'cost' as a "
|
||||
"top-level property - use 'content' wrapper instead"
|
||||
)
|
||||
# Must have 'content' wrapper
|
||||
assert "content" in response_200, (
|
||||
"/spend/calculate 200 response must have a 'content' field"
|
||||
)
|
||||
content = response_200["content"]
|
||||
assert "application/json" in content
|
||||
assert "schema" in content["application/json"]
|
||||
break
|
||||
else:
|
||||
pytest.fail("/spend/calculate route not found in router")
|
||||
|
||||
|
||||
class TestCredentialEndpointsOpenAPISchema:
|
||||
"""Test /credentials endpoints have correct path parameters."""
|
||||
|
||||
def test_by_name_and_by_model_are_separate_handlers(self):
|
||||
"""
|
||||
/credentials/by_name/{credential_name} and /credentials/by_model/{model_id}
|
||||
must be separate handler functions so each only declares its own path params.
|
||||
"""
|
||||
from litellm.proxy.credential_endpoints.endpoints import router
|
||||
|
||||
by_name_routes = []
|
||||
by_model_routes = []
|
||||
for route in router.routes:
|
||||
if not hasattr(route, "path"):
|
||||
continue
|
||||
if "by_name" in route.path:
|
||||
by_name_routes.append(route)
|
||||
elif "by_model" in route.path:
|
||||
by_model_routes.append(route)
|
||||
|
||||
assert len(by_name_routes) == 1, "Expected exactly one by_name route"
|
||||
assert len(by_model_routes) == 1, "Expected exactly one by_model route"
|
||||
|
||||
# They must be different endpoint functions
|
||||
by_name_endpoint = by_name_routes[0].endpoint
|
||||
by_model_endpoint = by_model_routes[0].endpoint
|
||||
assert by_name_endpoint is not by_model_endpoint, (
|
||||
"by_name and by_model must be separate handler functions "
|
||||
"to avoid path parameter conflicts in OpenAPI spec"
|
||||
)
|
||||
|
||||
def test_by_model_route_does_not_require_credential_name(self):
|
||||
"""
|
||||
The /credentials/by_model/{model_id} route must NOT have
|
||||
credential_name as a parameter.
|
||||
"""
|
||||
import inspect
|
||||
from litellm.proxy.credential_endpoints.endpoints import (
|
||||
get_credential_by_model,
|
||||
)
|
||||
|
||||
sig = inspect.signature(get_credential_by_model)
|
||||
param_names = list(sig.parameters.keys())
|
||||
assert "credential_name" not in param_names, (
|
||||
"get_credential_by_model must not have a credential_name parameter"
|
||||
)
|
||||
|
||||
def test_by_name_route_does_not_require_model_id(self):
|
||||
"""
|
||||
The /credentials/by_name/{credential_name} route must NOT have
|
||||
model_id as a parameter.
|
||||
"""
|
||||
import inspect
|
||||
from litellm.proxy.credential_endpoints.endpoints import (
|
||||
get_credential_by_name,
|
||||
)
|
||||
|
||||
sig = inspect.signature(get_credential_by_name)
|
||||
param_names = list(sig.parameters.keys())
|
||||
assert "model_id" not in param_names, (
|
||||
"get_credential_by_name must not have a model_id parameter"
|
||||
)
|
||||
|
||||
def test_by_model_has_model_id_path_param(self):
|
||||
"""The by_model handler must accept model_id as a path parameter."""
|
||||
import inspect
|
||||
from litellm.proxy.credential_endpoints.endpoints import (
|
||||
get_credential_by_model,
|
||||
)
|
||||
|
||||
sig = inspect.signature(get_credential_by_model)
|
||||
assert "model_id" in sig.parameters, (
|
||||
"get_credential_by_model must have a model_id parameter"
|
||||
)
|
||||
|
||||
def test_by_name_has_credential_name_path_param(self):
|
||||
"""The by_name handler must accept credential_name as a path parameter."""
|
||||
import inspect
|
||||
from litellm.proxy.credential_endpoints.endpoints import (
|
||||
get_credential_by_name,
|
||||
)
|
||||
|
||||
sig = inspect.signature(get_credential_by_name)
|
||||
assert "credential_name" in sig.parameters, (
|
||||
"get_credential_by_name must have a credential_name parameter"
|
||||
)
|
||||
Reference in New Issue
Block a user