Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_aws_external_id_embed_sagemaker

This commit is contained in:
mateo-berri 2026-08-29 00:17:05 -07:00
commit d7bd6ca614
5 changed files with 112 additions and 7 deletions

View file

@ -8,7 +8,7 @@
import asyncio
import traceback
from collections.abc import Mapping
from typing import Any, BinaryIO, Final, cast, get_args
from typing import Any, BinaryIO, Final, TypedDict, cast, get_args
import httpx
from fastapi import (
@ -23,6 +23,7 @@ from fastapi import (
status,
)
from pydantic import TypeAdapter
from typing_extensions import ReadOnly
import litellm
from litellm import CreateFileRequest, get_secret_str
@ -83,6 +84,13 @@ router: Final = APIRouter()
_MAX_BATCH_FILE_SIZE_MB_ADAPTER: Final = TypeAdapter(int | None)
class UploadedFileInfo(TypedDict):
filename: ReadOnly[str | None]
content_type: ReadOnly[str | None]
size: ReadOnly[int | None]
files_config = None
@ -526,6 +534,22 @@ async def create_file(
proxy_config=proxy_config,
)
uploaded_file_info: Final[UploadedFileInfo] = {
"filename": file.filename,
"content_type": file.content_type,
"size": file.size,
}
data["purpose"] = purpose
data["file"] = uploaded_file_info
hooked_data: Final = await proxy_logging_obj.pre_call_hook(
user_api_key_dict=user_api_key_dict,
data=data,
call_type="acreate_file",
)
data = hooked_data if hooked_data is not None else data
data.pop("purpose", None)
data.pop("file", None)
# /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

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

View file

@ -69,7 +69,7 @@ proxy = [
"mcp>=1.28.1,<2.0",
"litellm-proxy-extras==0.4.90",
"litellm-enterprise==0.1.61",
"RestrictedPython>=8.1,<9.0",
"RestrictedPython>=8.5,<9.0",
"rich>=13.9.4,<14.0",
"InquirerPy>=0.3.4,<1.0",
"polars>=1.38.1,<2.0",

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(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 respx.post("https://api.openai.com/v1/files").mock(
return_value=respx.MockResponse(
status_code=200,
json={
"id": "file-hooked",
"object": "file",
"bytes": 23,
"created_at": 1234567890,
"filename": "labels.jsonl",
"purpose": "user_data",
"status": "uploaded",
},
)
)
@respx.mock
def test_create_file_triggers_async_pre_call_hook(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_route = _setup_create_file_over_pre_call_hook(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"
assert provider_route.call_count == 1
forwarded_body = provider_route.calls.last.request.content
assert b"user_data" in forwarded_body
assert b"labels.jsonl" in forwarded_body
@respx.mock
def test_create_file_async_pre_call_hook_rejection_blocks_upload(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_route = _setup_create_file_over_pre_call_hook(monkeypatch, llm_router, RejectingHook())
response = _post_user_data_file()
assert response.status_code == 400, response.text
assert "file upload not allowed" in response.text
assert provider_route.call_count == 0

10
uv.lock generated
View file

@ -10,7 +10,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-08-24T20:19:42.376246Z"
exclude-newer = "2026-08-25T23:16:47.126855Z"
exclude-newer-span = "P3D"
[manifest]
@ -4558,7 +4558,7 @@ requires-dist = [
{ name = "redisvl", marker = "extra == 'extra-proxy'", specifier = ">=0.4.1,<1.0" },
{ name = "requests", marker = "extra == 'cli'", specifier = ">=2.32.0,<3.0" },
{ name = "resend", marker = "extra == 'extra-proxy'", specifier = ">=2.23.0,<3.0" },
{ name = "restrictedpython", marker = "extra == 'proxy'", specifier = ">=8.1,<9.0" },
{ name = "restrictedpython", marker = "extra == 'proxy'", specifier = ">=8.5,<9.0" },
{ name = "rich", marker = "extra == 'cli'", specifier = ">=13.9.4,<14.0" },
{ name = "rich", marker = "extra == 'proxy'", specifier = ">=13.9.4,<14.0" },
{ name = "rq", marker = "extra == 'proxy'", specifier = ">=2.7.0,<3.0" },
@ -8265,11 +8265,11 @@ wheels = [
[[package]]
name = "restrictedpython"
version = "8.1"
version = "8.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/1c/aec08bcb4ab14a1521579fbe21ceff2a634bb1f737f11cf7f9c8bb96e680/restrictedpython-8.1.tar.gz", hash = "sha256:4a69304aceacf6bee74bdf153c728221d4e3109b39acbfe00b3494927080d898", size = 838331, upload-time = "2025-10-19T14:11:32.531Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/3b/8e41f7cfabbb30b1013ebc7484303d6c87da2906ec432d69dea11d2f7d75/restrictedpython-8.5.tar.gz", hash = "sha256:4ed1269dbe3caa88db650d1af325198a952aeb1451eca05df0cfa65db4466215", size = 455879, upload-time = "2026-08-19T07:02:10.934Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/c0/3848f4006f7e164ee20833ca984067e4b3fc99fe7f1dfa88b4927e681299/restrictedpython-8.1-py3-none-any.whl", hash = "sha256:4769449c6cdb10f2071649ba386902befff0eff2a8fd6217989fa7b16aeae926", size = 27651, upload-time = "2025-10-19T14:11:30.201Z" },
{ url = "https://files.pythonhosted.org/packages/58/57/16ce3c721f5a33317e4110575d5c9976c0c45f7fd96ca2e0adeab06e6026/restrictedpython-8.5-py3-none-any.whl", hash = "sha256:6c70e0a3af13e830d37225788cdc8ab5804a8df4b500c135086eaef34b5c01e0", size = 30962, upload-time = "2026-08-19T07:02:09.553Z" },
]
[[package]]