mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(files): preserve managed deletion routing and response identity
This commit is contained in:
parent
4ab5719ff9
commit
7d3b68fea5
3 changed files with 118 additions and 2 deletions
|
|
@ -1779,7 +1779,16 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
# Remove conflicting keys from data to avoid duplicate keyword arguments
|
||||
filtered_data = {k: v for k, v in data.items() if k not in ("model", "file_id")}
|
||||
for model_id, model_file_id in specific_model_file_id_mapping.items():
|
||||
delete_response = await llm_router.afile_delete(model=model_id, file_id=model_file_id, **filtered_data) # type: ignore
|
||||
credentials = llm_router.get_deployment_credentials_with_provider(model_id=model_id)
|
||||
delete_data = {
|
||||
**{k: v for k, v in filtered_data.items() if k != "_litellm_internal_model_credentials"},
|
||||
**(
|
||||
{"_litellm_internal_model_credentials": MappingProxyType(dict(credentials))}
|
||||
if credentials is not None
|
||||
else {}
|
||||
),
|
||||
}
|
||||
delete_response = await llm_router.afile_delete(model=model_id, file_id=model_file_id, **delete_data)
|
||||
|
||||
stored_file_object = await self.delete_unified_file_id(file_id, litellm_parent_otel_span)
|
||||
|
||||
|
|
@ -1790,7 +1799,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
prom_logger.record_managed_file_deleted(result="success")
|
||||
|
||||
if stored_file_object:
|
||||
return stored_file_object
|
||||
return OpenAIFileObject.model_validate(stored_file_object).model_copy(update={"id": file_id})
|
||||
elif delete_response:
|
||||
delete_response.id = file_id
|
||||
return delete_response
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ output and error files returned by terminal batches. Bedrock deletion uses a sig
|
|||
restricted to the configured storage buckets and managed file prefixes. The low-RPM
|
||||
test submits with its restricted key and cleans up with the test administrator key
|
||||
|
||||
Managed deletion forwards the deployment's trusted bucket configuration and returns
|
||||
the requested managed file ID even when stored output metadata carries a provider ID
|
||||
|
||||
Azure input uploads request `expires_after` anchored to `created_at` with
|
||||
`seconds=1209600`, and the lifecycle tests check the returned expiry. This is a
|
||||
fallback for interrupted runs: immediate deletion remains the normal cleanup.
|
||||
|
|
|
|||
|
|
@ -1095,6 +1095,110 @@ async def test_afile_content_passes_trusted_model_credentials_to_router():
|
|||
assert trusted_credentials["s3_bucket_name"] == "my-bucket"
|
||||
|
||||
|
||||
def _managed_deletion_file_id(provider_file_id):
|
||||
from litellm.types.utils import SpecialEnums
|
||||
|
||||
value = SpecialEnums.LITELLM_MANAGED_FILE_COMPLETE_STR.value.format(
|
||||
"application/json", "test-file", "batch-model", provider_file_id, "model-123"
|
||||
)
|
||||
return base64.urlsafe_b64encode(value.encode()).decode().rstrip("=")
|
||||
|
||||
|
||||
def _managed_files_with_deletion_row(unified_file_id, provider_file_id, file_object):
|
||||
from litellm.caching import DualCache
|
||||
from litellm.models.managed_files import LiteLLM_ManagedFileTable
|
||||
from litellm_enterprise.proxy.hooks.managed_files import _PROXY_LiteLLMManagedFiles
|
||||
|
||||
row = LiteLLM_ManagedFileTable(
|
||||
unified_file_id=unified_file_id,
|
||||
model_mappings={"model-123": provider_file_id},
|
||||
flat_model_file_ids=[provider_file_id],
|
||||
file_object=file_object,
|
||||
)
|
||||
table = MagicMock(
|
||||
find_first=AsyncMock(return_value=row),
|
||||
delete=AsyncMock(),
|
||||
)
|
||||
return _PROXY_LiteLLMManagedFiles(
|
||||
internal_usage_cache=DualCache(),
|
||||
prisma_client=MagicMock(db=MagicMock(litellm_managedfiletable=table)),
|
||||
), table
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_delete_bedrock_uses_deployment_bucket_and_signed_s3_delete(monkeypatch):
|
||||
import httpx
|
||||
import respx
|
||||
|
||||
from litellm import Router
|
||||
|
||||
monkeypatch.delenv("AWS_S3_BUCKET_NAME", raising=False)
|
||||
monkeypatch.delenv("AWS_S3_OUTPUT_BUCKET_NAME", raising=False)
|
||||
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "bedrock-batch",
|
||||
"litellm_params": {
|
||||
"model": "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"aws_access_key_id": "AKIAEXAMPLE",
|
||||
"aws_secret_access_key": "secret",
|
||||
"aws_region_name": "us-west-2",
|
||||
"s3_bucket_name": "my-bucket",
|
||||
},
|
||||
"model_info": {"id": "model-123"},
|
||||
}
|
||||
],
|
||||
num_retries=0,
|
||||
)
|
||||
s3_uri = "s3://my-bucket/litellm-bedrock-files/input.jsonl"
|
||||
unified_file_id = _managed_deletion_file_id(s3_uri)
|
||||
managed_files, table = _managed_files_with_deletion_row(unified_file_id, s3_uri, None)
|
||||
with respx.mock:
|
||||
route = respx.delete(
|
||||
"https://s3.us-west-2.amazonaws.com/my-bucket/litellm-bedrock-files/input.jsonl"
|
||||
).mock(return_value=httpx.Response(204))
|
||||
response = await managed_files.afile_delete(
|
||||
file_id=unified_file_id,
|
||||
litellm_parent_otel_span=None,
|
||||
llm_router=router,
|
||||
_litellm_internal_model_credentials={"s3_bucket_name": "request-bucket"},
|
||||
)
|
||||
|
||||
assert len(route.calls) == 1
|
||||
assert route.calls[0].request.headers["Authorization"].startswith("AWS4-HMAC-SHA256")
|
||||
assert response.id == unified_file_id
|
||||
assert response.deleted is True
|
||||
table.delete.assert_awaited_once_with(where={"unified_file_id": unified_file_id})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_delete_returns_managed_id_for_stored_provider_output():
|
||||
from openai.types import FileDeleted
|
||||
|
||||
provider_file_id = "file-error-output"
|
||||
unified_file_id = _managed_deletion_file_id(provider_file_id)
|
||||
stored_file = _make_file_object(provider_file_id)
|
||||
managed_files, table = _managed_files_with_deletion_row(unified_file_id, provider_file_id, stored_file)
|
||||
router = MagicMock(
|
||||
get_deployment_credentials_with_provider=MagicMock(return_value=None),
|
||||
afile_delete=AsyncMock(return_value=FileDeleted(id=provider_file_id, object="file", deleted=True)),
|
||||
)
|
||||
response = await managed_files.afile_delete(
|
||||
file_id=unified_file_id,
|
||||
litellm_parent_otel_span=None,
|
||||
llm_router=router,
|
||||
_litellm_internal_model_credentials={"s3_bucket_name": "request-bucket"},
|
||||
)
|
||||
|
||||
assert response.id == unified_file_id
|
||||
assert response.object == "file"
|
||||
assert response.filename == stored_file.filename
|
||||
assert stored_file.id == provider_file_id
|
||||
router.afile_delete.assert_awaited_once_with(model="model-123", file_id=provider_file_id)
|
||||
table.delete.assert_awaited_once_with(where={"unified_file_id": unified_file_id})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_content_bedrock_unified_id_end_to_end(monkeypatch):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue