fix(proxy): trigger async_pre_call_hook on POST /v1/files uploads

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-28 01:00:12 +00:00
parent 22a349ee70
commit f198efee32
3 changed files with 93 additions and 0 deletions

View file

@ -526,6 +526,18 @@ async def create_file(
proxy_config=proxy_config,
)
hook_data: Final[dict] = {
**data,
"purpose": purpose,
"file": {"filename": file.filename, "content_type": file.content_type, "size": file.size},
}
hooked_data: Final[dict] = await proxy_logging_obj.pre_call_hook(
user_api_key_dict=user_api_key_dict,
data=hook_data,
call_type="acreate_file",
)
data = {k: v for k, v in hooked_data.items() if k not in ("purpose", "file")}
# /v1/files stores its proxy metadata under litellm_metadata, not metadata
request_metadata: Final = data.get("metadata") or data.get("litellm_metadata") or EMPTY_MAPPING
scan_result: Final = await _scan_batch_upload(

View file

@ -543,6 +543,8 @@ CallTypesLiteral = Literal[
"_arealtime",
"create_batch",
"acreate_batch",
"create_file",
"acreate_file",
"pass_through_endpoint",
"allm_passthrough_route",
"anthropic_messages",

View file

@ -4476,3 +4476,82 @@ def test_scoped_list_files_still_resolves_deployment_credentials(
provider_list.assert_awaited_once()
assert provider_list.await_args.kwargs["custom_llm_provider"] == "openai"
assert provider_list.await_args.kwargs["api_key"] == "openai_api_key"
def _post_user_data_file() -> httpx.Response:
return client.post(
"/v1/files",
files={"file": ("labels.jsonl", b'{"label": "restricted"}', "application/json")},
data={"purpose": "user_data"},
headers={"Authorization": "Bearer test-key"},
)
def _setup_create_file_over_pre_call_hook(mocker, monkeypatch, llm_router, hook):
setup_proxy_logging_object(monkeypatch, llm_router)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
monkeypatch.setattr(litellm, "callbacks", [hook])
monkeypatch.setattr(
"litellm.proxy.openai_files_endpoints.files_endpoints.files_config",
[{"custom_llm_provider": "openai", "api_key": "sk-test"}],
)
return mocker.patch(
"litellm.proxy.openai_files_endpoints.files_endpoints.litellm.acreate_file",
new=mocker.AsyncMock(
return_value=OpenAIFileObject(
id="file-hooked",
object="file",
bytes=23,
created_at=1234567890,
filename="labels.jsonl",
purpose="user_data",
status="uploaded",
)
),
)
def test_create_file_triggers_async_pre_call_hook(mocker: MockerFixture, monkeypatch, llm_router: Router):
"""`POST /v1/files` must run `async_pre_call_hook` so a hook can inspect the upload
before it reaches the provider (LIT-5916)."""
from litellm.integrations.custom_logger import CustomLogger
recorded: dict = {}
class RecordingHook(CustomLogger):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
recorded["call_type"] = call_type
recorded["purpose"] = data.get("purpose")
recorded["file"] = data.get("file")
provider_create = _setup_create_file_over_pre_call_hook(mocker, monkeypatch, llm_router, RecordingHook())
response = _post_user_data_file()
assert response.status_code == 200, response.text
assert recorded["call_type"] == "acreate_file"
assert recorded["purpose"] == "user_data"
assert recorded["file"]["filename"] == "labels.jsonl"
provider_create.assert_awaited_once()
forwarded = provider_create.await_args.kwargs
assert forwarded["purpose"] == "user_data"
assert forwarded["file"][0] == "labels.jsonl"
def test_create_file_async_pre_call_hook_rejection_blocks_upload(
mocker: MockerFixture, monkeypatch, llm_router: Router
):
"""A hook rejecting the upload must 400 before the file reaches the provider."""
from litellm.integrations.custom_logger import CustomLogger
class RejectingHook(CustomLogger):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
return "file upload not allowed"
provider_create = _setup_create_file_over_pre_call_hook(mocker, monkeypatch, llm_router, RejectingHook())
response = _post_user_data_file()
assert response.status_code == 400, response.text
assert "file upload not allowed" in response.text
provider_create.assert_not_awaited()