mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-12 18:25:41 +00:00
Merge pull request #22728 from BerriAI/litellm_batch_expiry_validation_followup
fix(proxy): improve team expiry enforcement validation
This commit is contained in:
@@ -128,12 +128,22 @@ async def create_batch( # noqa: PLR0915
|
||||
if enforced_batch_expiry is not None:
|
||||
if "anchor" not in enforced_batch_expiry or "seconds" not in enforced_batch_expiry:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": "enforced_batch_output_expires_after must contain 'anchor' and 'seconds' keys",
|
||||
"error": "Server configuration error: team metadata field 'enforced_batch_output_expires_after' is malformed - must contain 'anchor' and 'seconds' keys. Contact your team or proxy admin to fix this setting.",
|
||||
},
|
||||
)
|
||||
_create_batch_data["output_expires_after"] = enforced_batch_expiry
|
||||
if enforced_batch_expiry["anchor"] != "created_at":
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": f"Server configuration error: team metadata field 'enforced_batch_output_expires_after' has invalid anchor '{enforced_batch_expiry['anchor']}' - must be 'created_at'. Contact your team or proxy admin to fix this setting.",
|
||||
},
|
||||
)
|
||||
_create_batch_data["output_expires_after"] = {
|
||||
"anchor": "created_at",
|
||||
"seconds": int(enforced_batch_expiry["seconds"]),
|
||||
}
|
||||
|
||||
input_file_id = _create_batch_data.get("input_file_id", None)
|
||||
unified_file_id: Union[str, Literal[False]] = False
|
||||
|
||||
@@ -460,21 +460,21 @@ async def create_file( # noqa: PLR0915
|
||||
if enforced_file_expiry is not None:
|
||||
if "anchor" not in enforced_file_expiry or "seconds" not in enforced_file_expiry:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": "enforced_file_expires_after must contain 'anchor' and 'seconds' keys",
|
||||
"error": "Server configuration error: team metadata field 'enforced_file_expires_after' is malformed - must contain 'anchor' and 'seconds' keys. Contact your team or proxy admin to fix this setting.",
|
||||
},
|
||||
)
|
||||
if enforced_file_expiry["anchor"] != "created_at":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": f"enforced_file_expires_after anchor must be 'created_at', got '{enforced_file_expiry['anchor']}'",
|
||||
"error": f"Server configuration error: team metadata field 'enforced_file_expires_after' has invalid anchor '{enforced_file_expiry['anchor']}' - must be 'created_at'. Contact your team or proxy admin to fix this setting.",
|
||||
},
|
||||
)
|
||||
expires_after = FileExpiresAfter(
|
||||
anchor="created_at",
|
||||
seconds=enforced_file_expiry["seconds"],
|
||||
seconds=int(enforced_file_expiry["seconds"]),
|
||||
)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
|
||||
@@ -1303,3 +1303,121 @@ def test_file_no_team_setting_preserves_caller(
|
||||
)
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 86400
|
||||
|
||||
|
||||
def test_file_team_injects_when_caller_sends_nothing(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""Team enforcement applies even when caller sends no expiry."""
|
||||
expires_after = _post_file_with_team_metadata(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_file_expires_after": {
|
||||
"anchor": "created_at",
|
||||
"seconds": 3600,
|
||||
}
|
||||
},
|
||||
form_data={
|
||||
"purpose": "batch",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
},
|
||||
)
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 3600
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Team-level enforced_file_expires_after validation error tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _post_file_raw(monkeypatch, llm_router: Router, team_metadata: dict, form_data: dict):
|
||||
"""POST /v1/files and return the raw response (no status assertion)."""
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
dummy, _ = _make_capturing_managed_files()
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = dummy
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
user_key = UserAPIKeyAuth(api_key="test-key", team_metadata=team_metadata)
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: user_key
|
||||
|
||||
test_file = ("mydata.jsonl", b'{"prompt": "Hello"}', "application/json")
|
||||
try:
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
files={"file": test_file},
|
||||
data=form_data,
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def test_file_missing_anchor_key_returns_500(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""Missing 'anchor' key in team metadata returns 500."""
|
||||
response = _post_file_raw(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_file_expires_after": {"seconds": 3600},
|
||||
},
|
||||
form_data={
|
||||
"purpose": "batch",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "malformed" in response.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_file_missing_seconds_key_returns_500(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""Missing 'seconds' key in team metadata returns 500."""
|
||||
response = _post_file_raw(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_file_expires_after": {"anchor": "created_at"},
|
||||
},
|
||||
form_data={
|
||||
"purpose": "batch",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "malformed" in response.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_file_invalid_anchor_returns_500(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""Invalid anchor value in team metadata returns 500."""
|
||||
response = _post_file_raw(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_file_expires_after": {
|
||||
"anchor": "updated_at",
|
||||
"seconds": 3600,
|
||||
},
|
||||
},
|
||||
form_data={
|
||||
"purpose": "batch",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "created_at" in response.json()["error"]["message"]
|
||||
|
||||
@@ -160,3 +160,103 @@ class TestBatchEndpointTeamOverride:
|
||||
},
|
||||
)
|
||||
assert kwargs["output_expires_after"] == CALLER_EXPIRY
|
||||
|
||||
def test_team_injects_when_caller_sends_nothing(self, monkeypatch, llm_router):
|
||||
"""Team enforcement applies even when caller sends no expiry."""
|
||||
kwargs = self._post_batch(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_batch_output_expires_after": TEAM_EXPIRY,
|
||||
},
|
||||
request_body={
|
||||
"input_file_id": "file-abc123",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"completion_window": "24h",
|
||||
},
|
||||
)
|
||||
assert kwargs["output_expires_after"] == TEAM_EXPIRY
|
||||
|
||||
|
||||
class TestBatchEndpointTeamValidation:
|
||||
"""Verify validation errors for malformed team metadata on batch endpoint."""
|
||||
|
||||
def _post_batch_raw(
|
||||
self,
|
||||
monkeypatch,
|
||||
llm_router: Router,
|
||||
team_metadata: dict,
|
||||
request_body: dict,
|
||||
):
|
||||
"""POST /v1/batches and return the raw response (no status assertion)."""
|
||||
_setup_proxy(monkeypatch, llm_router)
|
||||
|
||||
user_key = UserAPIKeyAuth(
|
||||
api_key="test-key",
|
||||
team_metadata=team_metadata,
|
||||
)
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: user_key
|
||||
|
||||
async def mock_acreate_batch(**kwargs):
|
||||
return _make_batch_response()
|
||||
|
||||
monkeypatch.setattr(litellm, "acreate_batch", mock_acreate_batch)
|
||||
|
||||
try:
|
||||
response = client.post(
|
||||
"/v1/batches",
|
||||
json=request_body,
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
return response
|
||||
|
||||
_BATCH_BODY = {
|
||||
"input_file_id": "file-abc123",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"completion_window": "24h",
|
||||
}
|
||||
|
||||
def test_missing_anchor_key_returns_500(self, monkeypatch, llm_router):
|
||||
"""Missing 'anchor' key in team metadata returns 500."""
|
||||
response = self._post_batch_raw(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_batch_output_expires_after": {"seconds": 3600},
|
||||
},
|
||||
request_body=self._BATCH_BODY,
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "malformed" in response.json()["error"]["message"]
|
||||
|
||||
def test_missing_seconds_key_returns_500(self, monkeypatch, llm_router):
|
||||
"""Missing 'seconds' key in team metadata returns 500."""
|
||||
response = self._post_batch_raw(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_batch_output_expires_after": {"anchor": "created_at"},
|
||||
},
|
||||
request_body=self._BATCH_BODY,
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "malformed" in response.json()["error"]["message"]
|
||||
|
||||
def test_invalid_anchor_returns_500(self, monkeypatch, llm_router):
|
||||
"""Invalid anchor value in team metadata returns 500."""
|
||||
response = self._post_batch_raw(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={
|
||||
"enforced_batch_output_expires_after": {
|
||||
"anchor": "last_active_at",
|
||||
"seconds": 3600,
|
||||
},
|
||||
},
|
||||
request_body=self._BATCH_BODY,
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "created_at" in response.json()["error"]["message"]
|
||||
|
||||
Reference in New Issue
Block a user