mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
fix(azure_ai): bill FLUX.2 images in whole megapixels the way Azure meters them
Azure's request_meta for FLUX.2-pro edits showed that it rounds every generated image up to whole megapixels, bills a lone reference at its rounded-up megapixels capped at 4, and bills each reference of a multi-reference edit as exactly 1 MP. The per-pixel math under-billed small and fractional references and over-billed photos, for example a 4032x3024 reference logged 11.63 MP where Azure charged 4 The FLUX.2 edit now reports one pixel count per reference, and the Azure AI cost calculator applies the megapixel rule to FLUX.2 models only. A reference whose header reports zero pixels is billed as one unmeasured megapixel so it no longer voids the whole edit's reference cost azure_ai/flux.2-pro now bills $0.03 for the first generated megapixel and $0.015 for each extra one, matching Azure's price list, instead of a flat $0.04 per image
This commit is contained in:
parent
8f321466e3
commit
e0e0629c4c
6 changed files with 230 additions and 58 deletions
|
|
@ -43,7 +43,7 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
|
|||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.reference_image_pixels: int = 0
|
||||
self.reference_image_pixels: tuple[int, ...] = ()
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
return AzureFoundryFluxImageGenerationConfig().get_supported_openai_params(model)
|
||||
|
|
@ -123,7 +123,7 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
|
|||
raise ValueError(f"{model} supports at most {max_reference_images} reference images.")
|
||||
|
||||
reference_bytes: Final = tuple(self._read_image_bytes(reference_image) for reference_image in images)
|
||||
self.reference_image_pixels = sum(_pixel_count(image_bytes) for image_bytes in reference_bytes)
|
||||
self.reference_image_pixels = tuple(_pixel_count(image_bytes) for image_bytes in reference_bytes)
|
||||
reference_images: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
"input_image" if index == 1 else f"input_image_{index}": base64.b64encode(image_bytes).decode("utf-8")
|
||||
|
|
@ -198,8 +198,8 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
|
|||
|
||||
def _pixel_count(image_bytes: bytes) -> int:
|
||||
dimensions: Final = image_dimensions_from_bytes(image_bytes)
|
||||
if dimensions is None:
|
||||
verbose_logger.warning("Could not read the dimensions of a FLUX.2 reference image, billing it as one megapixel")
|
||||
return UNMEASURED_REFERENCE_IMAGE_PIXELS
|
||||
width, height = dimensions
|
||||
return width * height
|
||||
pixels: Final = dimensions[0] * dimensions[1] if dimensions is not None else 0
|
||||
if pixels > 0:
|
||||
return pixels
|
||||
verbose_logger.warning("Could not read the dimensions of a FLUX.2 reference image, billing it as one megapixel")
|
||||
return UNMEASURED_REFERENCE_IMAGE_PIXELS
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import math
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
from typing import Annotated, Any, Final
|
||||
|
||||
from pydantic import Field, TypeAdapter, ValidationError
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
||||
|
|
@ -8,25 +11,58 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
|||
resolve_image_model_info,
|
||||
)
|
||||
from litellm.llms.azure_ai.image_edit.flux2_transformation import REFERENCE_IMAGE_PIXELS_HIDDEN_PARAM
|
||||
from litellm.llms.azure_ai.image_generation.flux_transformation import AzureFoundryFluxImageGenerationConfig
|
||||
from litellm.types.utils import ImageResponse, ModelInfo
|
||||
|
||||
MEGAPIXEL: Final = 1024 * 1024
|
||||
MAX_LONE_REFERENCE_MEGAPIXELS: Final = 4
|
||||
_REFERENCE_PIXELS: Final = TypeAdapter(tuple[Annotated[int, Field(strict=True, gt=0)], ...])
|
||||
|
||||
def _pixel_rate(resolved: ModelInfo, cost_key: str) -> float:
|
||||
|
||||
def _price(resolved: ModelInfo, cost_key: str) -> float | None:
|
||||
deployment_price: Final = _get_cost_per_unit(resolved, cost_key, default_value=None)
|
||||
if deployment_price is not None:
|
||||
return deployment_price
|
||||
model_cost_key: Final = resolved.get("key")
|
||||
shared_entry: Final = litellm.model_cost.get(model_cost_key) if model_cost_key is not None else None
|
||||
if shared_entry is None:
|
||||
return 0.0
|
||||
return shared_entry.get(cost_key) or 0.0
|
||||
return None
|
||||
return shared_entry.get(cost_key)
|
||||
|
||||
|
||||
def _pixel_rate(resolved: ModelInfo, cost_key: str) -> float:
|
||||
return _price(resolved, cost_key) or 0.0
|
||||
|
||||
|
||||
def _billable_megapixels(pixels: int) -> int:
|
||||
return math.ceil(pixels / MEGAPIXEL)
|
||||
|
||||
|
||||
def _billable_reference_megapixels(reference_pixels: tuple[int, ...]) -> int:
|
||||
# Azure's FLUX.2 request_meta (2026-09-25) bills a lone reference at no more than 4 MP, and each reference of a
|
||||
# multi-reference edit as exactly 1 MP whatever its size
|
||||
match reference_pixels:
|
||||
case ():
|
||||
return 0
|
||||
case (lone_reference,):
|
||||
return min(_billable_megapixels(lone_reference), MAX_LONE_REFERENCE_MEGAPIXELS)
|
||||
case _:
|
||||
return len(reference_pixels)
|
||||
|
||||
|
||||
def _reference_cost(resolved: ModelInfo, image_response: ImageResponse) -> float:
|
||||
pixels: Final = image_response._hidden_params.get(REFERENCE_IMAGE_PIXELS_HIDDEN_PARAM)
|
||||
if type(pixels) is not int or pixels <= 0:
|
||||
reported_pixels: Final = image_response._hidden_params.get(REFERENCE_IMAGE_PIXELS_HIDDEN_PARAM)
|
||||
if reported_pixels is None:
|
||||
return 0.0
|
||||
return _pixel_rate(resolved, "input_cost_per_reference_pixel") * pixels
|
||||
try:
|
||||
reference_pixels: Final = _REFERENCE_PIXELS.validate_python(reported_pixels)
|
||||
except ValidationError:
|
||||
return 0.0
|
||||
return (
|
||||
_pixel_rate(resolved, "input_cost_per_reference_pixel")
|
||||
* MEGAPIXEL
|
||||
* _billable_reference_megapixels(reference_pixels)
|
||||
)
|
||||
|
||||
|
||||
def cost_calculator(
|
||||
|
|
@ -79,6 +115,9 @@ def _generated_cost(
|
|||
model_info: ModelInfo | None,
|
||||
) -> float:
|
||||
num_images: Final = n if n is not None else len(image_response.data or ())
|
||||
pixel_size: Final = _output_size(size, optional_params, image_response)
|
||||
if AzureFoundryFluxImageGenerationConfig.is_flux2_model(model):
|
||||
return num_images * _flux2_image_cost(resolved, _size_pixels(pixel_size))
|
||||
output_cost_per_image: Final[float] = resolved.get("output_cost_per_image") or 0.0
|
||||
if output_cost_per_image:
|
||||
return output_cost_per_image * num_images
|
||||
|
|
@ -87,13 +126,6 @@ def _generated_cost(
|
|||
|
||||
from litellm.cost_calculator import default_image_cost_calculator
|
||||
|
||||
width: Final = optional_params.get("width") if optional_params else None
|
||||
height: Final = optional_params.get("height") if optional_params else None
|
||||
pixel_size: Final = (
|
||||
f"{width}x{height}"
|
||||
if type(width) is int and type(height) is int and width > 0 and height > 0
|
||||
else size or image_response.size
|
||||
)
|
||||
return default_image_cost_calculator(
|
||||
model=resolved.get("key", model),
|
||||
custom_llm_provider=litellm.LlmProviders.AZURE_AI.value,
|
||||
|
|
@ -101,3 +133,25 @@ def _generated_cost(
|
|||
n=num_images,
|
||||
model_info=model_info,
|
||||
)
|
||||
|
||||
|
||||
def _flux2_image_cost(resolved: ModelInfo, pixels: int) -> float:
|
||||
megapixel_rate: Final = _pixel_rate(resolved, "input_cost_per_pixel") * MEGAPIXEL
|
||||
first_megapixel_price: Final = _price(resolved, "output_cost_per_image")
|
||||
first_megapixel: Final = megapixel_rate if first_megapixel_price is None else first_megapixel_price
|
||||
return first_megapixel + megapixel_rate * (_billable_megapixels(pixels) - 1)
|
||||
|
||||
|
||||
def _output_size(
|
||||
size: str | None, optional_params: Mapping[str, object] | None, image_response: ImageResponse
|
||||
) -> str | None:
|
||||
width: Final = optional_params.get("width") if optional_params else None
|
||||
height: Final = optional_params.get("height") if optional_params else None
|
||||
if type(width) is int and type(height) is int and width > 0 and height > 0:
|
||||
return f"{width}x{height}"
|
||||
return size or image_response.size
|
||||
|
||||
|
||||
def _size_pixels(size: str | None) -> int:
|
||||
width, height = (int(dimension) for dimension in (size or "1024x1024").replace("-x-", "x").split("x"))
|
||||
return width * height
|
||||
|
|
|
|||
|
|
@ -11371,11 +11371,12 @@
|
|||
]
|
||||
},
|
||||
"azure_ai/flux.2-pro": {
|
||||
"input_cost_per_pixel": 1.430511474609375e-08,
|
||||
"input_cost_per_reference_pixel": 1.430511474609375e-08,
|
||||
"litellm_provider": "azure_ai",
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.04,
|
||||
"source": "https://ai.azure.com/explore/models/flux.2-pro/version/1/registry/azureml-blackforestlabs",
|
||||
"output_cost_per_image": 0.03,
|
||||
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/black-forest-labs/",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11371,11 +11371,12 @@
|
|||
]
|
||||
},
|
||||
"azure_ai/flux.2-pro": {
|
||||
"input_cost_per_pixel": 1.430511474609375e-08,
|
||||
"input_cost_per_reference_pixel": 1.430511474609375e-08,
|
||||
"litellm_provider": "azure_ai",
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.04,
|
||||
"source": "https://ai.azure.com/explore/models/flux.2-pro/version/1/registry/azureml-blackforestlabs",
|
||||
"output_cost_per_image": 0.03,
|
||||
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/black-forest-labs/",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -55,6 +55,15 @@ def _edit_response(reference_image_pixels: object) -> ImageResponse:
|
|||
)
|
||||
|
||||
|
||||
def _pro_megapixel_rates() -> tuple[float, float, float]:
|
||||
row: Final = litellm.model_cost["azure_ai/flux.2-pro"]
|
||||
return (
|
||||
row["output_cost_per_image"],
|
||||
row["input_cost_per_pixel"] * 1024 * 1024,
|
||||
row["input_cost_per_reference_pixel"] * 1024 * 1024,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "provider_path"),
|
||||
[
|
||||
|
|
@ -225,11 +234,68 @@ def test_flux2_flex_cost_accepts_lowercase_model_spelling():
|
|||
cost: Final = litellm.completion_cost(
|
||||
model="azure_ai/flux.2-flex",
|
||||
completion_response=response,
|
||||
optional_params={"width": 2048, "height": 1024, "num_images": 2},
|
||||
call_type="image_generation",
|
||||
)
|
||||
|
||||
assert cost == pytest.approx(_flex_pixel_rate() * 2048 * 1024 * 2)
|
||||
|
||||
|
||||
def test_flux2_flex_generation_rounds_each_image_up_to_whole_megapixels():
|
||||
response: Final = ImageResponse(data=[ImageObject(b64_json="aW1n"), ImageObject(b64_json="aW1n")])
|
||||
|
||||
cost: Final = litellm.completion_cost(
|
||||
model="azure_ai/FLUX.2-flex",
|
||||
completion_response=response,
|
||||
optional_params={"width": 1536, "height": 1024, "num_images": 2},
|
||||
call_type="image_generation",
|
||||
)
|
||||
|
||||
assert cost == pytest.approx(_flex_pixel_rate() * 1536 * 1024 * 2)
|
||||
assert cost == pytest.approx(_flex_pixel_rate() * 2 * 1024 * 1024 * 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("size", "megapixels"), (("256x256", 1), ("1024x1280", 2), ("2048x2048", 4)))
|
||||
def test_flux2_pro_generation_bills_the_first_megapixel_then_each_additional_one(size: str, megapixels: int):
|
||||
first, additional, _reference = _pro_megapixel_rates()
|
||||
|
||||
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
|
||||
model="flux.2-pro",
|
||||
completion_response=ImageResponse(data=[ImageObject(b64_json="aW1n")]),
|
||||
custom_llm_provider="azure_ai",
|
||||
size=size,
|
||||
call_type="image_generation",
|
||||
)
|
||||
|
||||
assert cost == pytest.approx(first + additional * (megapixels - 1))
|
||||
|
||||
|
||||
# Billable megapixels as Azure's request_meta reported them for FLUX.2-pro edits on 2026-09-25
|
||||
@pytest.mark.parametrize(
|
||||
("size", "reference_pixels", "output_megapixels", "reference_megapixels"),
|
||||
(
|
||||
pytest.param("1024x1024", (1024 * 1024,), 1, 1, id="one-whole-megapixel-reference"),
|
||||
pytest.param("1024x1024", (1024 * 1280,), 1, 2, id="fractional-reference-rounds-up"),
|
||||
pytest.param("1024x1280", (1024 * 1280,), 2, 2, id="fractional-output-rounds-up"),
|
||||
pytest.param("1024x1024", (4032 * 3024,), 1, 4, id="lone-reference-caps-at-four-megapixels"),
|
||||
pytest.param("1024x1024", (1024 * 1280,) * 2, 1, 2, id="each-of-several-references-is-one-megapixel"),
|
||||
pytest.param("1024x1024", (1024 * 1024, 4032 * 3024), 1, 2, id="large-reference-among-several"),
|
||||
pytest.param("1024x1024", (1024 * 1280,) * 3, 1, 3, id="three-references"),
|
||||
),
|
||||
)
|
||||
def test_flux2_pro_edit_bills_the_megapixels_azure_meters(
|
||||
size: str, reference_pixels: tuple[int, ...], output_megapixels: int, reference_megapixels: int
|
||||
):
|
||||
first, additional, reference = _pro_megapixel_rates()
|
||||
|
||||
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
|
||||
model="flux.2-pro",
|
||||
completion_response=_edit_response(reference_pixels),
|
||||
custom_llm_provider="azure_ai",
|
||||
size=size,
|
||||
call_type="image_edit",
|
||||
)
|
||||
|
||||
assert cost == pytest.approx(first + additional * (output_megapixels - 1) + reference * reference_megapixels)
|
||||
|
||||
|
||||
def test_flux2_flex_reference_rate_matches_generated_rate():
|
||||
|
|
@ -240,28 +306,31 @@ def test_flux2_flex_edit_bills_reference_pixels_on_top_of_generated_pixels():
|
|||
generated_only: Final = _flex_edit_cost(ImageResponse(data=[ImageObject(b64_json="aW1n")], size="1024x1024"))
|
||||
|
||||
assert generated_only == pytest.approx(_flex_pixel_rate() * 1024 * 1024)
|
||||
assert _flex_edit_cost(_edit_response(1024 * 1024)) - generated_only == pytest.approx(
|
||||
assert _flex_edit_cost(_edit_response((1024 * 1024,))) - generated_only == pytest.approx(
|
||||
_flex_reference_rate() * 1024 * 1024
|
||||
)
|
||||
assert _flex_edit_cost(_edit_response(3 * 1024 * 1024)) - generated_only == pytest.approx(
|
||||
assert _flex_edit_cost(_edit_response((3 * 1024 * 1024,))) - generated_only == pytest.approx(
|
||||
_flex_reference_rate() * 3 * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
def test_flux2_flex_edit_reference_cost_scales_with_reference_pixels_not_image_count():
|
||||
def test_flux2_flex_edit_reference_cost_does_not_scale_with_image_count():
|
||||
two_outputs: Final = ImageResponse(
|
||||
data=[ImageObject(b64_json="aW1n"), ImageObject(b64_json="aW1n")],
|
||||
size="1024x1024",
|
||||
hidden_params={"reference_image_pixels": 1536 * 1024},
|
||||
hidden_params={"reference_image_pixels": (2048 * 1024,)},
|
||||
)
|
||||
|
||||
assert _flex_edit_cost(two_outputs) == pytest.approx(
|
||||
_flex_pixel_rate() * 1024 * 1024 * 2 + _flex_reference_rate() * 1536 * 1024
|
||||
_flex_pixel_rate() * 1024 * 1024 * 2 + _flex_reference_rate() * 2048 * 1024
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reference_image_pixels", (0, -1, True, None, 1048576.0, "1048576"))
|
||||
def test_flux2_flex_edit_bills_only_a_positive_integer_reference_count(reference_image_pixels: object):
|
||||
@pytest.mark.parametrize(
|
||||
"reference_image_pixels",
|
||||
((), (0,), (-1,), (True,), None, (1048576.0,), ("1048576",), 1048576, (1048576, "1048576"), (1048576, 0)),
|
||||
)
|
||||
def test_flux2_flex_edit_bills_only_positive_integer_reference_counts(reference_image_pixels: object):
|
||||
generated_only: Final = _flex_edit_cost(ImageResponse(data=[ImageObject(b64_json="aW1n")], size="1024x1024"))
|
||||
|
||||
assert _flex_edit_cost(_edit_response(reference_image_pixels)) == generated_only
|
||||
|
|
@ -271,7 +340,7 @@ def test_flux2_flex_edit_ignores_reference_pixels_when_provider_reports_token_us
|
|||
response: Final = ImageResponse(
|
||||
data=[ImageObject(b64_json="aW1n")],
|
||||
size="1024x1024",
|
||||
hidden_params={"reference_image_pixels": 1024 * 1024},
|
||||
hidden_params={"reference_image_pixels": (1024 * 1024,)},
|
||||
usage=ImageUsage(
|
||||
input_tokens=150,
|
||||
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=100, text_tokens=50),
|
||||
|
|
@ -294,7 +363,7 @@ def test_flux2_flex_edit_reads_the_reference_rate_from_its_own_catalog_key(monke
|
|||
litellm.get_model_info.cache_clear()
|
||||
_invalidate_model_cost_lowercase_map()
|
||||
|
||||
assert _flex_edit_cost(_edit_response(1024 * 1024)) == pytest.approx(
|
||||
assert _flex_edit_cost(_edit_response((1024 * 1024,))) == pytest.approx(
|
||||
_flex_pixel_rate() * 1024 * 1024 + 3e-07 * 1024 * 1024
|
||||
)
|
||||
|
||||
|
|
@ -303,7 +372,7 @@ def test_flux2_flex_edit_prices_output_from_the_request_size_not_the_reference()
|
|||
large_reference_small_output: Final = ImageResponse(
|
||||
data=[ImageObject(b64_json="aW1n")],
|
||||
size="1024x1024",
|
||||
hidden_params={"reference_image_pixels": 2048 * 2048},
|
||||
hidden_params={"reference_image_pixels": (2048 * 2048,)},
|
||||
)
|
||||
|
||||
assert _flex_edit_cost(large_reference_small_output) == pytest.approx(
|
||||
|
|
@ -312,14 +381,14 @@ def test_flux2_flex_edit_prices_output_from_the_request_size_not_the_reference()
|
|||
|
||||
|
||||
def test_flux2_flex_edit_honors_explicit_zero_deployment_generated_rate():
|
||||
cost: Final = _flex_edit_cost(_edit_response(1024 * 1024), model_info={"input_cost_per_pixel": 0.0})
|
||||
cost: Final = _flex_edit_cost(_edit_response((1024 * 1024,)), model_info={"input_cost_per_pixel": 0.0})
|
||||
|
||||
assert cost == pytest.approx(_flex_reference_rate() * 1024 * 1024)
|
||||
|
||||
|
||||
def test_flux2_flex_edit_prefers_deployment_reference_rate():
|
||||
cost: Final = _flex_edit_cost(
|
||||
_edit_response(1024 * 1024),
|
||||
_edit_response((1024 * 1024,)),
|
||||
model_info={"input_cost_per_pixel": 2e-07, "input_cost_per_reference_pixel": 3e-07},
|
||||
)
|
||||
|
||||
|
|
@ -327,13 +396,13 @@ def test_flux2_flex_edit_prefers_deployment_reference_rate():
|
|||
|
||||
|
||||
def test_flux2_flex_edit_deployment_reference_rate_alone_keeps_catalog_generated_rate():
|
||||
cost: Final = _flex_edit_cost(_edit_response(1024 * 1024), model_info={"input_cost_per_reference_pixel": 3e-07})
|
||||
cost: Final = _flex_edit_cost(_edit_response((1024 * 1024,)), model_info={"input_cost_per_reference_pixel": 3e-07})
|
||||
|
||||
assert cost == pytest.approx(_flex_pixel_rate() * 1024 * 1024 + 3e-07 * 1024 * 1024)
|
||||
|
||||
|
||||
def test_flux2_flex_edit_honors_explicit_zero_deployment_reference_rate():
|
||||
cost: Final = _flex_edit_cost(_edit_response(1024 * 1024), model_info={"input_cost_per_reference_pixel": 0.0})
|
||||
cost: Final = _flex_edit_cost(_edit_response((1024 * 1024,)), model_info={"input_cost_per_reference_pixel": 0.0})
|
||||
|
||||
assert cost == pytest.approx(_flex_pixel_rate() * 1024 * 1024)
|
||||
|
||||
|
|
@ -341,7 +410,7 @@ def test_flux2_flex_edit_honors_explicit_zero_deployment_reference_rate():
|
|||
def test_unlisted_azure_ai_model_bills_deployment_reference_rate() -> None:
|
||||
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
|
||||
model="unlisted-flux-deployment",
|
||||
completion_response=_edit_response(1024 * 1024),
|
||||
completion_response=_edit_response((1024 * 1024,)),
|
||||
custom_llm_provider="azure_ai",
|
||||
size="1024x1024",
|
||||
call_type="image_edit",
|
||||
|
|
@ -355,7 +424,7 @@ def test_unlisted_azure_ai_model_bills_deployment_reference_rate() -> None:
|
|||
def test_flat_priced_flux_edit_ignores_reference_pixels(model: str):
|
||||
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
|
||||
model=model,
|
||||
completion_response=_edit_response(4 * 1024 * 1024),
|
||||
completion_response=_edit_response((4 * 1024 * 1024,)),
|
||||
custom_llm_provider="azure_ai",
|
||||
size="1024x1024",
|
||||
call_type="image_edit",
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ def test_flux2_image_edit_preserves_controls_and_pixel_cost(dimensions: Mapping[
|
|||
generated_rate, reference_rate = _flex_rates()
|
||||
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 2048 * 1024 * 2 + reference_rate * 512 * 512
|
||||
generated_rate * 2048 * 1024 * 2 + reference_rate * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ def _edit_ok(request: httpx.Request) -> httpx.Response:
|
|||
return httpx.Response(200, json={"data": [{"b64_json": "aW1n"}]})
|
||||
|
||||
|
||||
def test_flux2_image_edit_bills_every_reference_by_its_header_dimensions():
|
||||
def test_flux2_image_edit_measures_every_reference_but_bills_each_of_several_as_one_megapixel():
|
||||
sent: Final[dict[str, object]] = {}
|
||||
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
|
|
@ -250,13 +250,12 @@ def test_flux2_image_edit_bills_every_reference_by_its_header_dimensions():
|
|||
size="1024x1024",
|
||||
)
|
||||
generated_rate, reference_rate = _flex_rates()
|
||||
reference_pixels: Final = 1024 * 1024 + 800 * 600 + 640 * 480
|
||||
|
||||
assert sent["input_image"] == base64.b64encode(references[0]).decode()
|
||||
assert sent["input_image_3"] == base64.b64encode(references[2]).decode()
|
||||
assert response._hidden_params["reference_image_pixels"] == reference_pixels
|
||||
assert response._hidden_params["reference_image_pixels"] == (1024 * 1024, 800 * 600, 640 * 480)
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 1024 * 1024 + reference_rate * reference_pixels
|
||||
generated_rate * 1024 * 1024 + reference_rate * 3 * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -273,28 +272,77 @@ def test_flux2_image_edit_reads_streams_once_and_still_measures_them():
|
|||
)
|
||||
generated_rate, reference_rate = _flex_rates()
|
||||
|
||||
assert response._hidden_params["reference_image_pixels"] == 2048 * 2048
|
||||
assert response._hidden_params["reference_image_pixels"] == (2048 * 2048,)
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 1024 * 1024 + reference_rate * 2048 * 2048
|
||||
)
|
||||
|
||||
|
||||
def test_flux2_image_edit_bills_unmeasurable_references_as_one_megapixel_each():
|
||||
@pytest.mark.parametrize(
|
||||
("reference", "billed_megapixels"),
|
||||
(
|
||||
pytest.param(_png(640, 640), 1, id="small-reference-rounds-up"),
|
||||
pytest.param(_png(1024, 1280), 2, id="fractional-reference-rounds-up"),
|
||||
pytest.param(_jpeg(4032, 3024), 4, id="photo-reference-caps-at-four-megapixels"),
|
||||
),
|
||||
)
|
||||
def test_flux2_image_edit_bills_a_lone_reference_in_whole_megapixels(reference: bytes, billed_megapixels: int):
|
||||
response: Final = litellm.image_edit(
|
||||
model="azure_ai/FLUX.2-flex",
|
||||
image=[_png(640, 640), b"BM not a parseable header", b"\x89PNG\r\n\x1a\n\x00\x00"],
|
||||
prompt="Blend every reference",
|
||||
image=reference,
|
||||
prompt="Make it a watercolor",
|
||||
api_key="test-key",
|
||||
api_base="https://example.services.ai.azure.com",
|
||||
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
|
||||
size="1024x1024",
|
||||
)
|
||||
generated_rate, reference_rate = _flex_rates()
|
||||
reference_pixels: Final = 640 * 640 + 2 * UNMEASURED_REFERENCE_IMAGE_PIXELS
|
||||
|
||||
assert response._hidden_params["reference_image_pixels"] == reference_pixels
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 1024 * 1024 + reference_rate * reference_pixels
|
||||
generated_rate * 1024 * 1024 + reference_rate * billed_megapixels * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reference",
|
||||
(
|
||||
pytest.param(b"BM not a parseable header", id="unsupported-format"),
|
||||
pytest.param(b"\x89PNG\r\n\x1a\n\x00\x00", id="truncated-header"),
|
||||
pytest.param(_png(0, 640), id="zero-width-header"),
|
||||
),
|
||||
)
|
||||
def test_flux2_image_edit_bills_a_lone_unmeasurable_reference_as_one_megapixel(reference: bytes):
|
||||
response: Final = litellm.image_edit(
|
||||
model="azure_ai/FLUX.2-flex",
|
||||
image=[reference],
|
||||
prompt="Make it a watercolor",
|
||||
api_key="test-key",
|
||||
api_base="https://example.services.ai.azure.com",
|
||||
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
|
||||
size="1024x1024",
|
||||
)
|
||||
generated_rate, reference_rate = _flex_rates()
|
||||
|
||||
assert response._hidden_params["reference_image_pixels"] == (UNMEASURED_REFERENCE_IMAGE_PIXELS,)
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 1024 * 1024 + reference_rate * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
def test_flux2_image_edit_still_bills_every_reference_when_one_header_reports_zero_pixels():
|
||||
response: Final = litellm.image_edit(
|
||||
model="azure_ai/FLUX.2-flex",
|
||||
image=[_png(1024, 1024), _jpeg(640, 0)],
|
||||
prompt="Blend both references",
|
||||
api_key="test-key",
|
||||
api_base="https://example.services.ai.azure.com",
|
||||
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
|
||||
size="1024x1024",
|
||||
)
|
||||
generated_rate, reference_rate = _flex_rates()
|
||||
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 1024 * 1024 + reference_rate * 2 * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -324,7 +372,7 @@ def test_flux2_image_edit_resends_and_rebills_a_reused_stream(stream_position: s
|
|||
)
|
||||
|
||||
assert sent_images == [base64.b64encode(reference).decode()] * 2
|
||||
assert [response._hidden_params["reference_image_pixels"] for response in responses] == [2048 * 1024] * 2
|
||||
assert [response._hidden_params["reference_image_pixels"] for response in responses] == [(2048 * 1024,)] * 2
|
||||
|
||||
|
||||
def test_flux2_pro_image_edit_bills_references_on_the_pro_reference_rate():
|
||||
|
|
@ -338,11 +386,10 @@ def test_flux2_pro_image_edit_bills_references_on_the_pro_reference_rate():
|
|||
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
|
||||
size="1024x1024",
|
||||
)
|
||||
reference_pixels: Final = 1024 * 1024 + 4032 * 3024
|
||||
|
||||
assert pro_row["input_cost_per_reference_pixel"] > 0
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
pro_row["output_cost_per_image"] + pro_row["input_cost_per_reference_pixel"] * reference_pixels
|
||||
pro_row["output_cost_per_image"] + pro_row["input_cost_per_reference_pixel"] * 2 * 1024 * 1024
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -401,7 +448,7 @@ async def test_flux2_aimage_edit_bills_references_like_image_edit():
|
|||
)
|
||||
generated_rate, reference_rate = _flex_rates()
|
||||
|
||||
assert response._hidden_params["reference_image_pixels"] == 2 * 1024 * 1024
|
||||
assert response._hidden_params["reference_image_pixels"] == (1024 * 1024, 1024 * 1024)
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
generated_rate * 1024 * 1024 + reference_rate * 2 * 1024 * 1024
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue