mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
test(proxy/files): update stubs to handle IO[bytes] file content
This commit is contained in:
parent
3bde9e1827
commit
8477facb95
1 changed files with 235 additions and 113 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Union
|
||||
from unittest.mock import ANY
|
||||
|
||||
import pytest
|
||||
|
|
@ -21,6 +23,19 @@ from litellm.proxy.proxy_server import app
|
|||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def _get_file_size(file_content: Union[bytes, io.IOBase]) -> int:
|
||||
"""Return file size without loading IO objects fully into memory."""
|
||||
if isinstance(file_content, io.IOBase):
|
||||
pos = file_content.tell()
|
||||
file_content.seek(0, 2)
|
||||
size = file_content.tell()
|
||||
file_content.seek(pos)
|
||||
return size
|
||||
return len(file_content) # type: ignore[arg-type]
|
||||
|
||||
|
||||
from litellm.caching.caching import DualCache
|
||||
from litellm.proxy.proxy_server import hash_token
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
|
@ -106,16 +121,18 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
|
|||
Asserts 'create_file' is called with the correct arguments
|
||||
"""
|
||||
import litellm
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm import Router
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
|
||||
# Mock create_file as an async function
|
||||
mock_create_file = mocker.patch("litellm.files.main.create_file", new=mocker.AsyncMock())
|
||||
mock_create_file = mocker.patch(
|
||||
"litellm.files.main.create_file", new=mocker.AsyncMock()
|
||||
)
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
|
|
@ -127,7 +144,14 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
|
|||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
# Handle both dict and object forms of create_file_request
|
||||
if isinstance(create_file_request, dict):
|
||||
file_data = create_file_request.get("file")
|
||||
|
|
@ -135,7 +159,7 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
|
|||
else:
|
||||
file_data = create_file_request.file
|
||||
purpose_data = create_file_request.purpose
|
||||
|
||||
|
||||
# Call the mocked litellm.files.main.create_file to ensure asserts work
|
||||
await litellm.files.main.create_file(
|
||||
custom_llm_provider="azure",
|
||||
|
|
@ -153,26 +177,31 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
|
|||
)
|
||||
# Return a dummy response object as needed by the test
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="dummy-id",
|
||||
object="file",
|
||||
bytes=len(file_data[1]) if file_data else 0,
|
||||
bytes=_get_file_size(file_data[1]) if file_data else 0,
|
||||
created_at=1234567890,
|
||||
filename=file_data[0] if file_data else "test.wav",
|
||||
purpose=purpose_data,
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
# Manually add the hook to the proxy_hook_mapping
|
||||
|
|
@ -245,8 +274,8 @@ def test_target_storage_invokes_storage_backend(
|
|||
"""
|
||||
Ensure target_storage is parsed and invokes the storage backend service.
|
||||
"""
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
|
|
@ -304,8 +333,8 @@ def test_target_storage_with_target_models(
|
|||
"""
|
||||
Ensure target_storage and target_model_names are parsed and passed through.
|
||||
"""
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
|
|
@ -611,7 +640,9 @@ def test_create_file_for_each_model(
|
|||
assert openai_call_found, "OpenAI call not found with expected parameters"
|
||||
|
||||
|
||||
def test_create_file_with_expires_after(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
def test_create_file_with_expires_after(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""
|
||||
Test that expires_after is properly parsed and passed through when creating a file
|
||||
"""
|
||||
|
|
@ -624,18 +655,25 @@ def test_create_file_with_expires_after(mocker: MockerFixture, monkeypatch, llm_
|
|||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
# Verify expires_after is in the request
|
||||
if isinstance(create_file_request, dict):
|
||||
expires_after = create_file_request.get("expires_after")
|
||||
else:
|
||||
expires_after = getattr(create_file_request, "expires_after", None)
|
||||
|
||||
|
||||
# Verify expires_after was passed correctly
|
||||
assert expires_after is not None, "expires_after should be in the request"
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 2592000
|
||||
|
||||
|
||||
# Return a dummy response
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
|
|
@ -646,17 +684,21 @@ def test_create_file_with_expires_after(mocker: MockerFixture, monkeypatch, llm_
|
|||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
|
|
@ -688,7 +730,9 @@ def test_create_file_with_expires_after(mocker: MockerFixture, monkeypatch, llm_
|
|||
assert result["purpose"] == "fine-tune"
|
||||
|
||||
|
||||
def test_create_file_with_expires_after_missing_anchor(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
def test_create_file_with_expires_after_missing_anchor(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""
|
||||
Test that an error is returned when expires_after[anchor] is missing
|
||||
"""
|
||||
|
|
@ -717,10 +761,15 @@ def test_create_file_with_expires_after_missing_anchor(mocker: MockerFixture, mo
|
|||
|
||||
assert response.status_code == 400
|
||||
error_detail = response.json()
|
||||
assert "expires_after" in error_detail["error"]["message"].lower() or "both" in error_detail["error"]["message"].lower()
|
||||
assert (
|
||||
"expires_after" in error_detail["error"]["message"].lower()
|
||||
or "both" in error_detail["error"]["message"].lower()
|
||||
)
|
||||
|
||||
|
||||
def test_create_file_with_expires_after_missing_seconds(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
def test_create_file_with_expires_after_missing_seconds(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""
|
||||
Test that an error is returned when expires_after[seconds] is missing
|
||||
"""
|
||||
|
|
@ -749,10 +798,15 @@ def test_create_file_with_expires_after_missing_seconds(mocker: MockerFixture, m
|
|||
|
||||
assert response.status_code == 400
|
||||
error_detail = response.json()
|
||||
assert "expires_after" in error_detail["error"]["message"].lower() or "both" in error_detail["error"]["message"].lower()
|
||||
assert (
|
||||
"expires_after" in error_detail["error"]["message"].lower()
|
||||
or "both" in error_detail["error"]["message"].lower()
|
||||
)
|
||||
|
||||
|
||||
def test_create_file_with_expires_after_valid_values(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
def test_create_file_with_expires_after_valid_values(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""
|
||||
Test that expires_after works with valid anchor and seconds values
|
||||
"""
|
||||
|
|
@ -765,18 +819,25 @@ def test_create_file_with_expires_after_valid_values(mocker: MockerFixture, monk
|
|||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
# Verify expires_after is in the request
|
||||
if isinstance(create_file_request, dict):
|
||||
expires_after = create_file_request.get("expires_after")
|
||||
else:
|
||||
expires_after = getattr(create_file_request, "expires_after", None)
|
||||
|
||||
|
||||
# Verify expires_after was passed correctly
|
||||
assert expires_after is not None, "expires_after should be in the request"
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 3600
|
||||
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
object="file",
|
||||
|
|
@ -786,17 +847,21 @@ def test_create_file_with_expires_after_valid_values(mocker: MockerFixture, monk
|
|||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
|
|
@ -827,7 +892,9 @@ def test_create_file_with_expires_after_valid_values(mocker: MockerFixture, monk
|
|||
assert result["purpose"] == "fine-tune"
|
||||
|
||||
|
||||
def test_create_file_without_expires_after(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
def test_create_file_without_expires_after(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""
|
||||
Test that file creation works normally without expires_after
|
||||
"""
|
||||
|
|
@ -840,16 +907,25 @@ def test_create_file_without_expires_after(mocker: MockerFixture, monkeypatch, l
|
|||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
# Verify expires_after is None when not provided
|
||||
if isinstance(create_file_request, dict):
|
||||
expires_after = create_file_request.get("expires_after")
|
||||
else:
|
||||
expires_after = getattr(create_file_request, "expires_after", None)
|
||||
|
||||
|
||||
# expires_after should be None when not provided
|
||||
assert expires_after is None, "expires_after should be None when not provided"
|
||||
|
||||
assert (
|
||||
expires_after is None
|
||||
), "expires_after should be None when not provided"
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
object="file",
|
||||
|
|
@ -859,17 +935,21 @@ def test_create_file_without_expires_after(mocker: MockerFixture, monkeypatch, l
|
|||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
|
|
@ -898,11 +978,13 @@ def test_create_file_without_expires_after(mocker: MockerFixture, monkeypatch, l
|
|||
assert result["purpose"] == "fine-tune"
|
||||
|
||||
|
||||
def test_managed_files_with_loadbalancing(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
def test_managed_files_with_loadbalancing(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
"""
|
||||
Test that managed files work with loadbalancing when both target_model_names
|
||||
and enable_loadbalancing_on_batch_endpoints are enabled.
|
||||
|
||||
|
||||
This ensures that the priority order is correct:
|
||||
- managed files should take precedence over deprecated loadbalancing
|
||||
- managed files internally use llm_router.acreate_file() which provides loadbalancing
|
||||
|
|
@ -912,28 +994,34 @@ def test_managed_files_with_loadbalancing(mocker: MockerFixture, monkeypatch, ll
|
|||
|
||||
# Enable loadbalancing on batch endpoints
|
||||
monkeypatch.setattr("litellm.enable_loadbalancing_on_batch_endpoints", True)
|
||||
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
|
||||
# Track calls to verify loadbalancing through router
|
||||
router_acreate_file_calls = []
|
||||
|
||||
|
||||
class ManagedFilesWithLoadbalancing(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
# Verify we receive the target model names
|
||||
assert len(target_model_names_list) > 0, "Should have target_model_names_list"
|
||||
|
||||
assert (
|
||||
len(target_model_names_list) > 0
|
||||
), "Should have target_model_names_list"
|
||||
|
||||
# Simulate what managed files does - call llm_router.acreate_file for each model
|
||||
# This is where loadbalancing happens internally
|
||||
for model in target_model_names_list:
|
||||
router_acreate_file_calls.append({
|
||||
"model": model,
|
||||
"via_router": True
|
||||
})
|
||||
|
||||
router_acreate_file_calls.append({"model": model, "via_router": True})
|
||||
|
||||
# Return a managed file ID (base64 encoded)
|
||||
return OpenAIFileObject(
|
||||
id="litellm_managed_file_abc123",
|
||||
|
|
@ -944,23 +1032,29 @@ def test_managed_files_with_loadbalancing(mocker: MockerFixture, monkeypatch, ll
|
|||
purpose="batch",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = ManagedFilesWithLoadbalancing()
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = (
|
||||
ManagedFilesWithLoadbalancing()
|
||||
)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
|
|
@ -971,12 +1065,12 @@ def test_managed_files_with_loadbalancing(mocker: MockerFixture, monkeypatch, ll
|
|||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
api_key="test-key", user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
# Create batch file content
|
||||
test_file_content = b'{"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hello"}]}}'
|
||||
test_file = ("batch_data.jsonl", test_file_content, "application/jsonl")
|
||||
|
||||
|
||||
# Make request with both target_model_names AND enable_loadbalancing_on_batch_endpoints
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
|
|
@ -987,7 +1081,7 @@ def test_managed_files_with_loadbalancing(mocker: MockerFixture, monkeypatch, ll
|
|||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
|
||||
# Verify success
|
||||
assert response.status_code == 200, response.text
|
||||
finally:
|
||||
|
|
@ -995,13 +1089,17 @@ def test_managed_files_with_loadbalancing(mocker: MockerFixture, monkeypatch, ll
|
|||
result = response.json()
|
||||
assert result["id"] == "litellm_managed_file_abc123"
|
||||
assert result["purpose"] == "batch"
|
||||
|
||||
|
||||
# Verify that managed files was called (via router for loadbalancing)
|
||||
# This proves that managed files took precedence over deprecated loadbalancing
|
||||
assert len(router_acreate_file_calls) == 2, "Should have called router for both models"
|
||||
assert (
|
||||
len(router_acreate_file_calls) == 2
|
||||
), "Should have called router for both models"
|
||||
assert router_acreate_file_calls[0]["model"] == "azure-gpt-3-5-turbo"
|
||||
assert router_acreate_file_calls[1]["model"] == "gpt-3.5-turbo"
|
||||
assert all(call["via_router"] for call in router_acreate_file_calls), "All calls should go through router"
|
||||
assert all(
|
||||
call["via_router"] for call in router_acreate_file_calls
|
||||
), "All calls should go through router"
|
||||
|
||||
|
||||
def test_create_file_with_nested_litellm_metadata(
|
||||
|
|
@ -1009,22 +1107,29 @@ def test_create_file_with_nested_litellm_metadata(
|
|||
):
|
||||
"""
|
||||
Test that nested litellm_metadata is correctly parsed from form data in bracket notation.
|
||||
|
||||
|
||||
Regression test for: litellm_metadata[spend_logs_metadata][owner] format should be
|
||||
correctly parsed into nested dictionary structure.
|
||||
"""
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
|
||||
captured_litellm_metadata = {}
|
||||
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
# Capture litellm_metadata for verification
|
||||
if isinstance(create_file_request, dict):
|
||||
captured_litellm_metadata.update(
|
||||
|
|
@ -1034,7 +1139,7 @@ def test_create_file_with_nested_litellm_metadata(
|
|||
captured_litellm_metadata.update(
|
||||
getattr(create_file_request, "litellm_metadata", {})
|
||||
)
|
||||
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="file-test-123",
|
||||
object="file",
|
||||
|
|
@ -1044,28 +1149,32 @@ def test_create_file_with_nested_litellm_metadata(
|
|||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
|
||||
test_file_content = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
test_file = ("test.jsonl", test_file_content, "application/jsonl")
|
||||
|
||||
|
||||
# Test with nested litellm_metadata in bracket notation
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
|
|
@ -1080,12 +1189,12 @@ def test_create_file_with_nested_litellm_metadata(
|
|||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
|
||||
# Verify success
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == "file-test-123"
|
||||
|
||||
|
||||
# Verify nested metadata was correctly parsed
|
||||
assert "spend_logs_metadata" in captured_litellm_metadata
|
||||
assert captured_litellm_metadata["spend_logs_metadata"]["owner"] == "john_doe"
|
||||
|
|
@ -1099,26 +1208,33 @@ def test_create_file_with_deep_nested_litellm_metadata(
|
|||
):
|
||||
"""
|
||||
Test that deeply nested litellm_metadata is correctly parsed from form data.
|
||||
|
||||
|
||||
Regression test for: litellm_metadata[a][b][c] format should be correctly parsed.
|
||||
"""
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
|
||||
captured_litellm_metadata = {}
|
||||
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
async def acreate_file(
|
||||
self,
|
||||
llm_router,
|
||||
create_file_request,
|
||||
target_model_names_list,
|
||||
litellm_parent_otel_span,
|
||||
user_api_key_dict,
|
||||
):
|
||||
if isinstance(create_file_request, dict):
|
||||
captured_litellm_metadata.update(
|
||||
create_file_request.get("litellm_metadata", {})
|
||||
|
|
@ -1127,7 +1243,7 @@ def test_create_file_with_deep_nested_litellm_metadata(
|
|||
captured_litellm_metadata.update(
|
||||
getattr(create_file_request, "litellm_metadata", {})
|
||||
)
|
||||
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="file-test-456",
|
||||
object="file",
|
||||
|
|
@ -1137,33 +1253,37 @@ def test_create_file_with_deep_nested_litellm_metadata(
|
|||
purpose="batch",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_delete(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span, llm_router, **data):
|
||||
|
||||
async def afile_content(
|
||||
self, file_id, litellm_parent_otel_span, llm_router, **data
|
||||
):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="test-user"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
test_file_content = b'{"custom_id": "req-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-3.5-turbo"}}'
|
||||
test_file = ("nested.jsonl", test_file_content, "application/jsonl")
|
||||
|
||||
|
||||
# Test with deeply nested metadata
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
|
|
@ -1177,12 +1297,12 @@ def test_create_file_with_deep_nested_litellm_metadata(
|
|||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
|
||||
# Verify success
|
||||
assert response.status_code == 200, response.text
|
||||
result = response.json()
|
||||
assert result["id"] == "file-test-456"
|
||||
|
||||
|
||||
# Verify deeply nested metadata was correctly parsed
|
||||
assert "config" in captured_litellm_metadata
|
||||
assert "database" in captured_litellm_metadata["config"]
|
||||
|
|
@ -1356,7 +1476,9 @@ def test_file_team_injects_when_caller_sends_nothing(
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _post_file_raw(monkeypatch, llm_router: Router, team_metadata: dict, form_data: dict):
|
||||
def _post_file_raw(
|
||||
monkeypatch, llm_router: Router, team_metadata: dict, form_data: dict
|
||||
):
|
||||
"""POST /v1/files and return the raw response (no status assertion)."""
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue