Merge pull request #22728 from BerriAI/litellm_batch_expiry_validation_followup

fix(proxy): improve team expiry enforcement validation
This commit is contained in:
ryan-crabbe
2026-03-04 10:16:02 -08:00
committed by GitHub
4 changed files with 236 additions and 8 deletions
+13 -3
View File
@@ -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"]