diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 6620db5ffa2..e12be6baf5d 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -750,9 +750,27 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): model_id=model_id, model_name=model_name, ) - await self.store_unified_file_id( # need to store otherwise any retrieve call will fail + + # Fetch the actual file object for the output file + file_object = None + try: + # Use litellm to retrieve the file object from the provider + from litellm import afile_retrieve + file_object = await afile_retrieve( + custom_llm_provider=model_name.split("/")[0] if model_name and "/" in model_name else "openai", + file_id=original_output_file_id + ) + verbose_logger.debug( + f"Successfully retrieved file object for output_file_id={original_output_file_id}" + ) + except Exception as e: + verbose_logger.warning( + f"Failed to retrieve file object for output_file_id={original_output_file_id}: {str(e)}. Storing with None and will fetch on-demand." + ) + + await self.store_unified_file_id( file_id=response.output_file_id, - file_object=None, + file_object=file_object, litellm_parent_otel_span=user_api_key_dict.parent_otel_span, model_mappings={model_id: original_output_file_id}, user_api_key_dict=user_api_key_dict, diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index ca646254d5d..81225159a7c 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -199,6 +199,7 @@ class AmazonAnthropicClaudeMessagesConfig( if beta_set: anthropic_messages_request["anthropic_beta"] = list(beta_set) + return anthropic_messages_request diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 03b9ac3deaa..086105042e8 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -573,6 +573,7 @@ async def list_batches( if target_model_names is None: raise ValueError("target_model_names is required for this routing scenario") model = target_model_names.split(",")[0] + data.pop("model", None) response = await llm_router.alist_batches( model=model, after=after, diff --git a/tests/batches_tests/test_openai_batches_and_files.py b/tests/batches_tests/test_openai_batches_and_files.py index 2f4f9bbcda1..055af024949 100644 --- a/tests/batches_tests/test_openai_batches_and_files.py +++ b/tests/batches_tests/test_openai_batches_and_files.py @@ -577,3 +577,73 @@ async def test_vertex_list_batches(monkeypatch): assert len(list_response["data"]) == 2 assert list_response["data"][0].id == "test-batch-id-456" assert list_response["data"][1].id == "test-batch-id-789" + + +@pytest.mark.asyncio +async def test_delete_batch_output_file(): + """ + Test that deleting a batch output file works correctly. + + This test verifies the fix for: + - When a batch is retrieved and has an output_file_id, the file object is properly stored + - The output file can be deleted without validation errors + - The file_object is fetched and stored with proper metadata instead of None + """ + litellm._turn_on_debug() + print("Testing delete batch output file") + + file_name = "openai_batch_completions.jsonl" + _current_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(_current_dir, file_name) + + # Create file for batch + file_obj = await litellm.acreate_file( + file=open(file_path, "rb"), + purpose="batch", + custom_llm_provider="openai", + ) + print("Response from creating file=", file_obj) + batch_input_file_id = file_obj.id + + # Create batch + create_batch_response = await litellm.acreate_batch( + completion_window="24h", + endpoint="/v1/chat/completions", + input_file_id=batch_input_file_id, + custom_llm_provider="openai", + ) + print("Batch created with ID=", create_batch_response.id) + + # Retrieve batch to get output_file_id + retrieved_batch = await litellm.aretrieve_batch( + batch_id=create_batch_response.id, + custom_llm_provider="openai" + ) + print("Retrieved batch=", retrieved_batch) + + # If batch has completed and has output file, test deleting it + if retrieved_batch.output_file_id: + print(f"Testing deletion of output file: {retrieved_batch.output_file_id}") + + # This is the key test - deleting the output file should work + # without validation errors (file_object should not be None) + delete_output_file_response = await litellm.afile_delete( + file_id=retrieved_batch.output_file_id, + custom_llm_provider="openai" + ) + + print("Delete output file response=", delete_output_file_response) + assert delete_output_file_response.id == retrieved_batch.output_file_id + assert delete_output_file_response.deleted is True or hasattr(delete_output_file_response, 'id') + print("✓ Successfully deleted batch output file") + else: + print("⚠ Batch has not completed yet or no output file available, skipping output file deletion test") + + # Clean up - delete the input file + delete_input_file_response = await litellm.afile_delete( + file_id=batch_input_file_id, + custom_llm_provider="openai" + ) + print("Delete input file response=", delete_input_file_response) + assert delete_input_file_response.id == batch_input_file_id + print("✓ Successfully deleted batch input file")