litellm/tests/unit/images/test_main.py
devin-ai-integration[bot] fa2c8984ba
test: move the unit half of 126 mixed legacy files into tests/unit (#45090)
* test: move the unit half of 126 mixed legacy files into tests/unit

* test: restore litellm globals that moved tests set

* test: finalize migration test cleanup

* test: restore original bodies of moved legacy tests

The move into tests/unit had rewritten 612 test bodies, and some of the rewrites dropped assertions. Each moved test now carries its original body from the legacy file, with only the imports, helpers, fake provider credentials and monkeypatched env it needs to run under tests/unit

test_timeout_streaming goes back to tests/local_testing because it needs the fake OpenAI endpoint server. The image payload fixture moves with its only user, and two tests that leaked global state (a registered model cost entry and queued logging tasks) are now isolated

* test: drop module imports shadowed by restored local imports

* test: assert on LiteLLM output in no-assertion moved tests and isolate leaks

Twenty no-assertion candidates get one assertion on the value LiteLLM returns, with the original lines unchanged. Four tests go back to their legacy files because they only check types or imports, write into the working directory, or cannot assert without a body change

Two moved tests leaked globals into later tests in the same worker, so monkeypatch fixtures now restore the retry-after header parser and the end user cost tracking flags

* test: drain queued logging tasks before the Phoenix span test

The moved Phoenix test counted spans from logging tasks that earlier tests had queued, so the drain fixture moves to tests/unit/conftest.py and both it and the Datadog batch test use it. test_factory_function goes back to its legacy file because its returned wrapper calls the real Assistants API and cannot be asserted on without a body change

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-07 14:07:43 -07:00

345 lines
13 KiB
Python

import asyncio
import json
from datetime import datetime
from typing import Final
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
import respx
from openai.types.image import Image
import litellm
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.types.utils import CallTypes, StandardLoggingPayload
import os
def test_image_generation_keeps_an_internal_prefixed_kwarg_out_of_the_provider_request(
respx_mock: respx.MockRouter,
) -> None:
api_base: Final = "http://localhost:12346/v1"
mock_route: Final = respx_mock.post(url__regex=rf"{api_base}/images/generations.*").mock(
return_value=httpx.Response(status_code=200, json={"created": 1712697600, "data": [{"b64_json": "aW1n"}]})
)
litellm.image_generation(
model="openai/gpt-image-1",
prompt="a red circle",
api_base=api_base,
api_key="fake_openai_api_key",
_litellm_undeclared_sentinel="internal",
)
assert mock_route.called
sent: Final = json.loads(respx_mock.calls[0].request.content)
assert "_litellm_undeclared_sentinel" not in sent, sent
assert sent["prompt"] == "a red circle"
def test_image_edit_prices_a_vertex_deployment_at_its_configured_location(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
api_base: Final = "http://localhost:12347/generateContent"
respx_mock.post(api_base).mock(
return_value=httpx.Response(
status_code=200,
json={"candidates": [{"content": {"parts": [{"inlineData": {"mimeType": "image/png", "data": "aW1n"}}]}}]},
)
)
monkeypatch.setitem(
litellm.model_cost,
"vertex_ai/gemini-fake-regional-edit-model",
{
"litellm_provider": "vertex_ai-language-models",
"mode": "image_generation",
"output_cost_per_image": 0.04,
"regional_endpoint_uplift_multiplier": 1.1,
},
)
def cost_at(location: str) -> float:
logging_obj: Final = Logging(
model="gemini-fake-regional-edit-model",
messages=[],
stream=False,
call_type=CallTypes.image_edit.value,
start_time=datetime.now(),
litellm_call_id=f"vertex-edit-{location}",
function_id="f",
)
response: Final = litellm.image_edit(
model="vertex_ai/gemini-fake-regional-edit-model",
image=b"\x89PNG\r\n\x1a\nfakepng",
prompt="make the circle blue",
api_base=api_base,
vertex_location=location,
litellm_logging_obj=logging_obj,
)
return logging_obj.response_cost_calculator(result=response)
assert cost_at("global") == pytest.approx(0.04)
assert cost_at("us-central1") == pytest.approx(0.044)
class TestCustomLogger(CustomLogger):
__test__ = False
def __init__(self) -> None:
super().__init__()
self.standard_logging_payload: StandardLoggingPayload | None = None
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
self.standard_logging_payload = kwargs.get("standard_logging_object")
class TestAimlImageGeneration:
def get_base_image_generation_call_args(self) -> dict:
return {"model": "aiml/flux-pro/v1.1"}
@pytest.mark.asyncio(scope="module")
@pytest.mark.flaky(retries=0)
async def test_basic_image_generation(self):
"""Test basic image generation"""
from unittest.mock import AsyncMock, patch
mock_aiml_response = {
"created": 1703658209,
"data": [{"url": "https://example.com/generated_image.png"}],
}
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = mock_aiml_response
mock_response.text = json.dumps(mock_aiml_response)
mock_response.headers = {}
with (
patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
new_callable=AsyncMock,
) as mock_async_post,
patch(
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post",
) as mock_sync_post,
):
mock_async_post.return_value = mock_response
mock_sync_post.return_value = mock_response
try:
litellm.turn_on_debug()
custom_logger = TestCustomLogger()
litellm.logging_callback_manager._reset_all_callbacks()
litellm.callbacks = [custom_logger]
base_image_generation_call_args = (
self.get_base_image_generation_call_args()
)
litellm.set_verbose = True
# Pass dummy api_key so validate_environment passes; HTTP is mocked
response = await litellm.aimage_generation(
**base_image_generation_call_args,
prompt="A image of a otter",
api_key="test-key-mocked-no-credits-needed",
)
print("FAL AI RESPONSE: ", response)
await asyncio.sleep(1)
# assert response._hidden_params["response_cost"] is not None
# assert response._hidden_params["response_cost"] > 0
# print("response_cost", response._hidden_params["response_cost"])
logged_standard_logging_payload = custom_logger.standard_logging_payload
print(
"logged_standard_logging_payload", logged_standard_logging_payload
)
assert logged_standard_logging_payload is not None
assert logged_standard_logging_payload["response_cost"] is not None
assert logged_standard_logging_payload["response_cost"] > 0
import openai
from openai.types.images_response import ImagesResponse
# print openai version
print("openai version=", openai.__version__)
response_dict = dict(response)
if "usage" in response_dict:
response_dict["usage"] = dict(response_dict["usage"])
print("response usage=", response_dict.get("usage"))
assert (
response.data is not None
) # type guard for iteration (base fails here if None)
for d in response.data:
assert isinstance(d, Image)
print("data in response.data", d)
assert d.b64_json is not None or d.url is not None
except litellm.RateLimitError as e:
pass
except litellm.ContentPolicyViolationError:
pass # Azure randomly raises these errors - skip when they occur
except litellm.InternalServerError:
pass
except Exception as e:
if "Your task failed as a result of our safety system." in str(e):
pass
else:
pytest.fail(f"An exception occurred - {str(e)}")
@pytest.mark.asyncio
async def test_aiml_image_generation_with_dynamic_api_key():
"""
Test that when api_key is passed as a dynamic parameter to aimage_generation,
it gets properly used for AIML provider authentication instead of falling back
to environment variables.
This test validates the fix for ensuring dynamic API keys are respected
when making image generation requests to the AIML provider.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
# Mock AIML response
mock_aiml_response = {
"created": 1703658209,
"data": [{"url": "https://example.com/generated_image.png"}],
}
# Track captured arguments
captured_headers = None
captured_url = None
captured_json_data = None
def capture_post_call(*args, **kwargs):
nonlocal captured_headers, captured_url, captured_json_data
captured_url = kwargs.get("url") or (args[0] if args else None)
captured_headers = kwargs.get("headers", {})
captured_json_data = kwargs.get("json", {})
# Create a mock response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = mock_aiml_response
mock_response.text = json.dumps(mock_aiml_response)
return mock_response
# Mock the HTTP client that actually makes the request (sync version for image generation)
with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post") as mock_post:
mock_post.side_effect = capture_post_call
# Test with dynamic api_key
test_api_key = "test-dynamic-api-key-12345"
response = await litellm.aimage_generation(
prompt="A cute baby sea otter",
model="aiml/flux-pro/v1.1",
api_key=test_api_key, # This should be used instead of env vars
)
# Validate the response (mocked response processing might not populate data correctly)
assert response is not None
# The most important validations: API key and endpoint usage
# These prove that the dynamic API key was properly used
assert captured_headers is not None
assert "Authorization" in captured_headers
assert captured_headers["Authorization"] == f"Bearer {test_api_key}"
print("TESTCAPTURED HEADERS", captured_headers)
# Validate the correct AIML endpoint was called
assert captured_url is not None
assert "api.aimlapi.com" in captured_url
assert "/v1/images/generations" in captured_url
# Validate the request data
assert captured_json_data is not None
assert captured_json_data["prompt"] == "A cute baby sea otter"
assert captured_json_data["model"] == "flux-pro/v1.1"
@pytest.mark.asyncio
async def test_aiml_openai_gpt_image_2_request_uses_openai_param_shape():
"""End-to-end check that ``aiml/openai/gpt-image-2`` keeps the upstream
OpenAI request shape (``size``/``n``/``response_format``) instead of
being remapped to the AI/ML flux schema (``image_size``/``num_images``/
``output_format``), and hits the correct upstream model name.
"""
import json as _json
from unittest.mock import MagicMock, patch
mock_aiml_response = {
"created": 1703658209,
"data": [{"url": "https://example.com/gpt-image-2.png"}],
}
captured = {}
def capture_post_call(*args, **kwargs):
captured["url"] = kwargs.get("url") or (args[0] if args else None)
captured["headers"] = kwargs.get("headers", {})
captured["json"] = kwargs.get("json", {})
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = mock_aiml_response
mock_response.text = _json.dumps(mock_aiml_response)
return mock_response
with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post") as mock_post:
mock_post.side_effect = capture_post_call
await litellm.aimage_generation(
prompt="A T-Rex relaxing on a beach",
model="aiml/openai/gpt-image-2",
api_key="test-key-mocked-no-credits-needed",
size="1024x1536",
quality="high",
response_format="b64_json",
n=1,
)
assert captured["url"] is not None
assert "api.aimlapi.com" in captured["url"]
assert "/v1/images/generations" in captured["url"]
body = captured["json"]
assert body["model"] == "openai/gpt-image-2"
assert body["prompt"] == "A T-Rex relaxing on a beach"
assert body["size"] == "1024x1536"
assert body["quality"] == "high"
assert body["response_format"] == "b64_json"
assert body["n"] == 1
assert "image_size" not in body
assert "num_images" not in body
assert "output_format" not in body
@pytest.mark.asyncio
async def test_azure_image_generation_request_body():
"""Azure deployment URL selects the model; JSON body omits ``model`` (#26316)."""
from litellm import aimage_generation
test_dir = os.path.dirname(__file__)
expected_path = os.path.join(test_dir, "request_payloads", "azure_gpt_image_1.json")
with open(expected_path, "r") as f:
expected_body = json.load(f)
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
new_callable=AsyncMock,
) as mock_post:
mock_post.side_effect = Exception("test")
with pytest.raises(litellm.APIConnectionError):
await aimage_generation(
model="azure/gpt-image-1",
prompt="test prompt",
api_base="https://example.azure.com",
api_key="test-key",
api_version="2025-04-01-preview",
)
mock_post.assert_called_once()
call_args = mock_post.call_args
request_json = call_args.kwargs.get("json", {})
assert request_json == expected_body