mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
feat(proxy): add team-level file expiry enforcement
This commit is contained in:
parent
3d15bcdb11
commit
08613b24cb
2 changed files with 145 additions and 1 deletions
|
|
@ -454,8 +454,17 @@ async def create_file( # noqa: PLR0915
|
|||
model=router_model, llm_router=llm_router
|
||||
)
|
||||
|
||||
# Apply team-level file expiry enforcement
|
||||
team_metadata = user_api_key_dict.team_metadata or {}
|
||||
enforced_file_expiry = team_metadata.get("enforced_file_expires_after")
|
||||
if enforced_file_expiry is not None:
|
||||
expires_after = FileExpiresAfter(
|
||||
anchor=enforced_file_expiry["anchor"],
|
||||
seconds=enforced_file_expiry["seconds"],
|
||||
)
|
||||
|
||||
_create_file_request = CreateFileRequest(
|
||||
file=file_data,
|
||||
file=file_data,
|
||||
purpose=cast(CREATE_FILE_REQUESTS_PURPOSE, purpose),
|
||||
expires_after=expires_after,
|
||||
**data
|
||||
|
|
|
|||
|
|
@ -1168,3 +1168,138 @@ def test_create_file_with_deep_nested_litellm_metadata(
|
|||
assert captured_litellm_metadata["config"]["database"]["port"] == "5432"
|
||||
assert "cache" in captured_litellm_metadata["config"]
|
||||
assert captured_litellm_metadata["config"]["cache"]["enabled"] == "true"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Team-level enforced_file_expires_after tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_capturing_managed_files():
|
||||
"""Create a DummyManagedFiles that captures the expires_after from the request."""
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
|
||||
captured = {}
|
||||
|
||||
class CapturingManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
if isinstance(create_file_request, dict):
|
||||
captured["expires_after"] = create_file_request.get("expires_after")
|
||||
else:
|
||||
captured["expires_after"] = getattr(
|
||||
create_file_request, "expires_after", None
|
||||
)
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
object="file",
|
||||
bytes=100,
|
||||
created_at=1234567890,
|
||||
filename="mydata.jsonl",
|
||||
purpose="batch",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
return CapturingManagedFiles(), captured
|
||||
|
||||
|
||||
def _post_file_with_team_metadata(
|
||||
monkeypatch,
|
||||
llm_router: Router,
|
||||
team_metadata: dict,
|
||||
form_data: dict,
|
||||
):
|
||||
"""POST /v1/files with given team_metadata, return captured expires_after."""
|
||||
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, captured = _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"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
return captured["expires_after"]
|
||||
|
||||
|
||||
def test_file_team_override_overrides_caller(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""Team enforced_file_expires_after wins over caller-provided value."""
|
||||
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",
|
||||
"expires_after[anchor]": "created_at",
|
||||
"expires_after[seconds]": "86400",
|
||||
},
|
||||
)
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 3600
|
||||
|
||||
|
||||
def test_file_no_team_setting_preserves_caller(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""No team setting = caller-provided expires_after passes through."""
|
||||
expires_after = _post_file_with_team_metadata(
|
||||
monkeypatch,
|
||||
llm_router,
|
||||
team_metadata={},
|
||||
form_data={
|
||||
"purpose": "batch",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
"expires_after[anchor]": "created_at",
|
||||
"expires_after[seconds]": "86400",
|
||||
},
|
||||
)
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 86400
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue