mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(files): list unscoped managed files
Read owner-scoped managed rows directly when no provider or model is supplied, avoiding an unauthenticated OpenAI fallback. Refs #35362
This commit is contained in:
parent
ff02d5cfc0
commit
f9f8320972
5 changed files with 130 additions and 23 deletions
|
|
@ -1365,12 +1365,23 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
|
||||
async def afile_list(
|
||||
self,
|
||||
purpose: Optional[OpenAIFilesPurpose],
|
||||
purpose: str | None,
|
||||
litellm_parent_otel_span: Optional[Span],
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
**data: Dict,
|
||||
) -> List[OpenAIFileObject]:
|
||||
"""Handled in files_endpoints.py"""
|
||||
return []
|
||||
) -> Dict[str, object]:
|
||||
owner_filter: Final = build_owner_filter(user_api_key_dict)
|
||||
if owner_filter is None:
|
||||
return build_list_page([])
|
||||
|
||||
rows: Final = await _managed_file_table(self.prisma_client).find_many(where=owner_filter)
|
||||
files: Final = [
|
||||
parsed_file_object.model_copy(update={"id": row.unified_file_id})
|
||||
for row in rows
|
||||
if (parsed_file_object := _parse_managed_file_object(row.file_object, row.unified_file_id)) is not None
|
||||
and (purpose is None or parsed_file_object.purpose == purpose)
|
||||
]
|
||||
return build_list_page(files)
|
||||
|
||||
def _is_batch_polling_enabled(self) -> bool:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from litellm.types.llms.openai import (
|
|||
FileContentRequest,
|
||||
OpenAICreateFileRequestOptionalParams,
|
||||
OpenAIFileObject,
|
||||
OpenAIFilesPurpose,
|
||||
)
|
||||
from litellm.types.utils import LlmProviders, ModelResponse
|
||||
|
||||
|
|
@ -240,10 +239,11 @@ class BaseFileEndpoints(ABC):
|
|||
@abstractmethod
|
||||
async def afile_list(
|
||||
self,
|
||||
purpose: OpenAIFilesPurpose | None,
|
||||
purpose: str | None,
|
||||
litellm_parent_otel_span: Span | None,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
**data: dict,
|
||||
) -> list[OpenAIFileObject]:
|
||||
) -> dict[str, object]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -1488,24 +1488,28 @@ async def list_files(
|
|||
or get_custom_llm_provider_from_request_headers(request=request)
|
||||
or get_custom_llm_provider_from_request_query(request=request)
|
||||
or await get_custom_llm_provider_from_request_body(request=request)
|
||||
or "openai"
|
||||
)
|
||||
managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
|
||||
if custom_llm_provider is None and isinstance(managed_files_obj, BaseFileEndpoints):
|
||||
response = await managed_files_obj.afile_list(
|
||||
purpose=purpose,
|
||||
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
else:
|
||||
resolved_custom_llm_provider: Final = custom_llm_provider or "openai"
|
||||
apply_team_provider_credentials(
|
||||
data=data,
|
||||
llm_router=llm_router,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
custom_llm_provider=resolved_custom_llm_provider,
|
||||
)
|
||||
|
||||
# No model/target_model_names pinned: resolve upstream credentials from
|
||||
# the team's deployment for this provider so the call is authenticated
|
||||
# against the team's own account (e.g. the team's openai deployment).
|
||||
apply_team_provider_credentials(
|
||||
data=data,
|
||||
llm_router=llm_router,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
response = await litellm.afile_list(
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
purpose=purpose,
|
||||
**data,
|
||||
)
|
||||
response = await litellm.afile_list(
|
||||
custom_llm_provider=resolved_custom_llm_provider,
|
||||
purpose=purpose,
|
||||
**data,
|
||||
)
|
||||
|
||||
if response is None:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -190,6 +190,39 @@ async def test_get_user_created_file_ids_remaps_stored_raw_provider_id_to_unifie
|
|||
assert files[0].purpose == raw_provider_object.purpose
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_returns_owner_scoped_managed_files():
|
||||
managed_files = _make_managed_files_instance()
|
||||
managed_files.prisma_client.db.litellm_managedfiletable.find_many = AsyncMock(
|
||||
return_value=[
|
||||
MagicMock(
|
||||
file_object=_make_file_object("file-provider-id").model_dump(),
|
||||
unified_file_id="unified-file-id",
|
||||
),
|
||||
MagicMock(
|
||||
file_object=_make_file_object("file-other-purpose").model_copy(
|
||||
update={"purpose": "batch"}
|
||||
).model_dump(),
|
||||
unified_file_id="unified-other-purpose",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
response = await managed_files.afile_list(
|
||||
purpose="batch_output",
|
||||
litellm_parent_otel_span=None,
|
||||
user_api_key_dict=_make_user_api_key_dict(),
|
||||
)
|
||||
|
||||
managed_files.prisma_client.db.litellm_managedfiletable.find_many.assert_awaited_once_with(
|
||||
where={"created_by": "test-user"}
|
||||
)
|
||||
assert [file.id for file in response["data"]] == ["unified-file-id"]
|
||||
assert response["first_id"] == "unified-file-id"
|
||||
assert response["last_id"] == "unified-file-id"
|
||||
assert response["has_more"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_managed_file_object_warning_omits_rejected_values(caplog):
|
||||
from litellm_enterprise.proxy.hooks.managed_files import (
|
||||
|
|
|
|||
|
|
@ -2468,6 +2468,65 @@ def test_list_files_without_target_model_names_uses_team_openai_deployment(
|
|||
proxy_logging_obj.post_call_failure_hook.assert_not_called()
|
||||
|
||||
|
||||
def test_unscoped_list_files_uses_managed_file_store(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
managed_file = OpenAIFileObject(
|
||||
id="unified-file-id",
|
||||
object="file",
|
||||
bytes=100,
|
||||
created_at=1700000000,
|
||||
filename="output.jsonl",
|
||||
purpose="batch_output",
|
||||
status="processed",
|
||||
)
|
||||
|
||||
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, llm_router)
|
||||
managed_files = mocker.MagicMock(spec=BaseFileEndpoints)
|
||||
managed_files.afile_list = mocker.AsyncMock(
|
||||
return_value={
|
||||
"object": "list",
|
||||
"data": [managed_file],
|
||||
"first_id": managed_file.id,
|
||||
"last_id": managed_file.id,
|
||||
"has_more": False,
|
||||
}
|
||||
)
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = managed_files
|
||||
proxy_logging_obj.update_request_status = mocker.AsyncMock()
|
||||
proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=None)
|
||||
proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
provider_list = mocker.patch.object(litellm, "afile_list", new=mocker.AsyncMock())
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
api_key="test-key",
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
user_id="test-user",
|
||||
)
|
||||
|
||||
try:
|
||||
response = client.get(
|
||||
"/v1/files",
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["data"][0]["id"] == "unified-file-id"
|
||||
managed_files.afile_list.assert_awaited_once()
|
||||
assert managed_files.afile_list.await_args.kwargs["user_api_key_dict"].user_id == "test-user"
|
||||
provider_list.assert_not_awaited()
|
||||
proxy_logging_obj.post_call_failure_hook.assert_not_called()
|
||||
|
||||
|
||||
def test_list_files_restricted_team_does_not_leak_global_openai_credentials(
|
||||
mocker: MockerFixture, monkeypatch
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue