This commit is contained in:
shrey 2026-10-28 22:18:46 -07:00 • committed by GitHub
commit 7b3d261cf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 453 additions and 9 deletions

View file

@ -32,6 +32,7 @@ pub enum InputUnit {
Character,
Image,
Pixel,
Megapixel,
Query { over_results: Option<NonZeroU64> },
Request,
Second,
@ -47,6 +48,8 @@ pub enum OutputUnit {
Character,
Image(Option<ImageSize>),
Pixel,
FirstMegapixel,
Megapixel,
Second(Option<Resolution>),
VideoSecond,
}

View file

@ -121,6 +121,9 @@ pub struct ModelInfoFields {
pub output_cost_per_video_per_second: Option<f64>,
pub output_cost_per_image: Option<f64>,
pub output_cost_per_pixel: Option<f64>,
pub output_cost_per_first_megapixel: Option<f64>,
pub output_cost_per_megapixel: Option<f64>,
pub input_cost_per_megapixel: Option<f64>,
pub output_cost_per_image_token: Option<f64>,
pub output_cost_per_image_token_batches: Option<f64>,
pub output_cost_per_video_token: Option<f64>,

View file

@ -61,8 +61,8 @@ struct RegistryDocument {
pub(crate) const FLAT_KEY_PATTERN: &str = concat!(
r"^(",
r"input_cost_per_((audio_|image_|video_)?token|character|image|pixel|query|request|second|text_message|audio_per_second|video_per_second(_above_\d+s_interval)?)",
r"|output_cost_per_((audio_|image_|video_|reasoning_)?token|character|image(_\d+(\.\d+)?K?)?|pixel|second(_\d+[pk])?|video_per_second)",
r"input_cost_per_((audio_|image_|video_)?token|character|image|pixel|megapixel|query|request|second|text_message|audio_per_second|video_per_second(_above_\d+s_interval)?)",
r"|output_cost_per_((audio_|image_|video_|reasoning_)?token|character|image(_\d+(\.\d+)?K?)?|pixel|(first_)?megapixel|second(_\d+[pk])?|video_per_second)",
r"|cache_read_input_(audio_|image_|video_)?token_cost",
r"|explicit_cache_read_input_token_cost",
r"|cache_creation_input_(audio_|image_|video_)?token_cost(_above_1hr|_above_12hr)?",
@ -186,6 +186,9 @@ mod tests {
"cache_creation_input_token_cost_above_1hr_above_200k_tokens"
)]
#[case::image_label("output_cost_per_image_0.5K")]
#[case::input_megapixel("input_cost_per_megapixel")]
#[case::first_output_megapixel("output_cost_per_first_megapixel")]
#[case::output_megapixel("output_cost_per_megapixel")]
#[case::resolution_tier("output_cost_per_second_4k_flex")]
#[case::feature_batch("annotation_cost_per_page_batches")]
#[case::video_interval("input_cost_per_video_per_second_above_8s_interval")]

View file

@ -180,6 +180,7 @@ fn input_unit(unit: &str) -> Option<InputUnit> {
"character" => Some(InputUnit::Character),
"image" => Some(InputUnit::Image),
"pixel" => Some(InputUnit::Pixel),
"megapixel" => Some(InputUnit::Megapixel),
"query" => Some(InputUnit::Query { over_results: None }),
"request" => Some(InputUnit::Request),
"second" => Some(InputUnit::Second),
@ -208,6 +209,8 @@ fn output_unit(unit: &str) -> Option<OutputUnit> {
"character" => Some(OutputUnit::Character),
"image" => Some(OutputUnit::Image(None)),
"pixel" => Some(OutputUnit::Pixel),
"first_megapixel" => Some(OutputUnit::FirstMegapixel),
"megapixel" => Some(OutputUnit::Megapixel),
"second" => Some(OutputUnit::Second(None)),
"video_per_second" => Some(OutputUnit::VideoSecond),
_ => None,
@ -289,6 +292,18 @@ mod tests {
"output_cost_per_image_token",
key(Meter::Output(OutputUnit::Token(TokenKind::Image)), Band::Base, None)
)]
#[case::input_megapixel(
"input_cost_per_megapixel",
key(Meter::Input(InputUnit::Megapixel), Band::Base, None)
)]
#[case::first_output_megapixel(
"output_cost_per_first_megapixel",
key(Meter::Output(OutputUnit::FirstMegapixel), Band::Base, None)
)]
#[case::output_megapixel(
"output_cost_per_megapixel",
key(Meter::Output(OutputUnit::Megapixel), Band::Base, None)
)]
#[case::resolution(
"output_cost_per_second_720p",
key(

View file

@ -1011,7 +1011,9 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
provider_config: Final = get_azure_image_generation_config(data.get("model", ""))
if provider_config is not None:
return provider_config.transform_image_generation_response(
from litellm.llms.azure_ai.image_generation.cost_calculator import record_megapixel_usage
image_response: Final = provider_config.transform_image_generation_response(
model=data.get("model", ""),
raw_response=httpx_response,
model_response=model_response or ImageResponse(),
@ -1021,6 +1023,8 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
litellm_params=data,
encoding=litellm.encoding,
)
record_megapixel_usage(image_response, httpx_response.content)
return image_response
else:
response = httpx_response.json()
@ -1167,12 +1171,14 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
additional_args={"complete_input_dict": data},
original_response=response,
)
# return response
return convert_to_model_response_object(
from litellm.llms.azure_ai.image_generation.cost_calculator import record_megapixel_usage
converted_response: Final = convert_to_model_response_object(
response_object=response,
model_response_object=model_response,
response_type="image_generation",
)
record_megapixel_usage(converted_response, httpx_response.content)
except AzureOpenAIError as e:
raise e
except Exception as e:
@ -1181,6 +1187,8 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
raise AzureOpenAIError(status_code=error_code, message=str(e))
else:
raise AzureOpenAIError(status_code=500, message=str(e))
else:
return converted_response
def audio_speech(
self,

View file

@ -4,6 +4,7 @@ from io import BufferedReader
from types import MappingProxyType
from typing import Any, Final
import httpx
from httpx._types import RequestFiles
import litellm
@ -11,14 +12,17 @@ from litellm.llms.azure_ai.common_utils import (
AzureFoundryModelInfo,
get_azure_ai_auth_headers,
)
from litellm.llms.azure_ai.image_generation.cost_calculator import record_megapixel_usage
from litellm.llms.azure_ai.image_generation.flux_transformation import (
AzureFoundryFluxImageGenerationConfig,
)
from litellm.llms.base_llm.image_edit.transformation import LiteLLMLoggingObj
from litellm.llms.openai.image_edit.transformation import OpenAIImageEditConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.images.main import ImageEditOptionalRequestParams
from litellm.types.llms.openai import FileTypes
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import ImageResponse
class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
@ -135,6 +139,16 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
return base64.b64encode(image_bytes).decode("utf-8")
def transform_image_edit_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ImageResponse:
image_response: Final = super().transform_image_edit_response(model, raw_response, logging_obj)
record_megapixel_usage(image_response, raw_response.content)
return image_response
def get_complete_url(
self,
model: str,

View file

@ -1,7 +1,11 @@
import math
from collections.abc import Mapping
from typing import Final
from pydantic import BaseModel, ConfigDict, Field, ValidationError
import litellm
from litellm.litellm_core_utils.hidden_params import set_hidden_param
from litellm.litellm_core_utils.llm_cost_calc.utils import (
calculate_image_response_cost_from_usage,
get_cost_per_unit,
@ -9,6 +13,58 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import (
)
from litellm.types.utils import ImageResponse, ModelInfo
_MEGAPIXEL_USAGE_HIDDEN_PARAM: Final = "megapixel_usage"
class _MegapixelUsage(BaseModel):
model_config = ConfigDict(frozen=True, allow_inf_nan=False)
input_mp: float | None = Field(default=None, ge=0)
output_mp: float = Field(gt=0)
class _MegapixelPrices(BaseModel):
model_config = ConfigDict(frozen=True)
output_cost_per_megapixel: float
output_cost_per_first_megapixel: float | None = None
input_cost_per_megapixel: float | None = None
class _MeteredImageResponse(BaseModel):
model_config = ConfigDict(frozen=True)
request_meta: _MegapixelUsage
def record_megapixel_usage(response: object, response_body: bytes | Mapping[str, object]) -> None:
try:
usage: Final = (
_MeteredImageResponse.model_validate_json(response_body)
if isinstance(response_body, bytes)
else _MeteredImageResponse.model_validate(response_body)
).request_meta
except ValidationError:
return
set_hidden_param(response, _MEGAPIXEL_USAGE_HIDDEN_PARAM, usage.model_dump())
def _megapixel_cost(model_info: ModelInfo, image_response: ImageResponse) -> float | None:
images: Final = len(image_response.data or ())
if not images:
return None
try:
prices: Final = _MegapixelPrices.model_validate(model_info)
usage: Final = _MegapixelUsage.model_validate(image_response.hidden_params.get(_MEGAPIXEL_USAGE_HIDDEN_PARAM))
except ValidationError:
return None
first_rate: Final = prices.output_cost_per_first_megapixel
output_mp_per_image: Final = usage.output_mp / images
return images * (
(prices.output_cost_per_megapixel if first_rate is None else first_rate)
+ prices.output_cost_per_megapixel * (math.ceil(output_mp_per_image) - 1)
) + (prices.input_cost_per_megapixel or 0.0) * math.ceil(usage.input_mp or 0)
def _input_cost_per_pixel(resolved: ModelInfo) -> float:
deployment_price: Final = get_cost_per_unit(resolved, "input_cost_per_pixel", default_value=None)
@ -48,6 +104,13 @@ def cost_calculator(
if token_based_cost is not None:
return token_based_cost
bills_megapixels: Final = model_info is None or any(
model_info.get(field) is not None for field in _MegapixelPrices.model_fields
)
megapixel_cost: Final = _megapixel_cost(_model_info, image_response) if bills_megapixels else None
if megapixel_cost is not None:
return megapixel_cost
num_images: Final = n if n is not None else len(image_response.data or ())
output_cost_per_image: Final[float] = _model_info.get("output_cost_per_image") or 0.0
if output_cost_per_image:

View file

@ -13,6 +13,7 @@ from litellm.llms.azure_ai.common_utils import (
api_key_header_for_base,
get_azure_ai_auth_headers,
)
from litellm.llms.azure_ai.image_generation.cost_calculator import record_megapixel_usage
from litellm.llms.base_llm.ocr.transformation import OCRResponse
from litellm.llms.base_llm.passthrough.transformation import (
BasePassthroughConfig,
@ -85,9 +86,16 @@ def relay_query_params(
return MappingProxyType({**(request_query_params or EMPTY_QUERY), "api-version": api_version})
def metered_image_response(body: Mapping[str, object]) -> ImageResponse:
image_response: Final = ImageResponse.model_validate(body)
record_megapixel_usage(image_response, body)
return image_response
FOUNDRY_RELAY_SHAPES: Final = (
RelayShape("/rerank", CallTypes.arerank, RerankResponse.model_validate),
RelayShape("/providers/blackforestlabs/v1/flux-2-pro", CallTypes.aimage_generation, ImageResponse.model_validate),
RelayShape("/providers/blackforestlabs/v1/flux-2-pro", CallTypes.aimage_generation, metered_image_response),
RelayShape("/providers/blackforestlabs/v1/flux-2-flex", CallTypes.aimage_generation, metered_image_response),
)

View file

@ -12463,21 +12463,26 @@
},
"azure_ai/flux.2-pro": {
"deprecation_date": "2027-01-20",
"input_cost_per_megapixel": 0.015,
"litellm_provider": "azure_ai",
"max_input_tokens": 32000,
"mode": "image_generation",
"output_cost_per_first_megapixel": 0.03,
"output_cost_per_image": 0.04,
"source": "https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/models-sold-directly-by-azure",
"output_cost_per_megapixel": 0.015,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/black-forest-labs/",
"supported_endpoints": [
"/v1/images/generations"
]
},
"azure_ai/FLUX.2-flex": {
"deprecation_date": "2027-01-20",
"input_cost_per_megapixel": 0.05,
"input_cost_per_pixel": 5e-08,
"litellm_provider": "azure_ai",
"max_input_tokens": 32000,
"mode": "image_generation",
"output_cost_per_megapixel": 0.05,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/black-forest-labs/",
"supported_endpoints": [
"/v1/images/generations",

View file

@ -430,6 +430,9 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
output_cost_per_character_above_128k_tokens: float | None # only for vertex ai models
output_cost_per_image: float | None
output_cost_per_pixel: ReadOnly[float | None]
output_cost_per_first_megapixel: ReadOnly[float | None]
output_cost_per_megapixel: ReadOnly[float | None]
input_cost_per_megapixel: ReadOnly[float | None]
output_cost_per_image_token: float | None
output_cost_per_image_token_batches: ReadOnly[float | None]
output_cost_per_video_token: float | None # for gemini omni models with video output
@ -4003,6 +4006,9 @@ class CustomPricingLiteLLMParams(MirroredPricingParams):
output_cost_per_image_1536: float | None = None
input_cost_per_pixel: float | None = None
output_cost_per_pixel: float | None = None
output_cost_per_first_megapixel: float | None = None
output_cost_per_megapixel: float | None = None
input_cost_per_megapixel: float | None = None
# Include all ModelInfoBase fields as optional
# This allows any model_info parameter to be set in litellm_params

View file

@ -6986,6 +6986,9 @@ def get_model_info_helper(
output_cost_per_video_per_second=_model_info.get("output_cost_per_video_per_second", None),
output_cost_per_image=_model_info.get("output_cost_per_image", None),
output_cost_per_pixel=_model_info.get("output_cost_per_pixel", None),
output_cost_per_first_megapixel=_model_info.get("output_cost_per_first_megapixel", None),
output_cost_per_megapixel=_model_info.get("output_cost_per_megapixel", None),
input_cost_per_megapixel=_model_info.get("input_cost_per_megapixel", None),
output_cost_per_image_token=_model_info.get("output_cost_per_image_token", None),
output_cost_per_image_token_batches=_model_info.get("output_cost_per_image_token_batches", None),
output_cost_per_video_token=_model_info.get("output_cost_per_video_token", None),

View file

@ -12463,21 +12463,26 @@
},
"azure_ai/flux.2-pro": {
"deprecation_date": "2027-01-20",
"input_cost_per_megapixel": 0.015,
"litellm_provider": "azure_ai",
"max_input_tokens": 32000,
"mode": "image_generation",
"output_cost_per_first_megapixel": 0.03,
"output_cost_per_image": 0.04,
"source": "https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/models-sold-directly-by-azure",
"output_cost_per_megapixel": 0.015,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/black-forest-labs/",
"supported_endpoints": [
"/v1/images/generations"
]
},
"azure_ai/FLUX.2-flex": {
"deprecation_date": "2027-01-20",
"input_cost_per_megapixel": 0.05,
"input_cost_per_pixel": 5e-08,
"litellm_provider": "azure_ai",
"max_input_tokens": 32000,
"mode": "image_generation",
"output_cost_per_megapixel": 0.05,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/black-forest-labs/",
"supported_endpoints": [
"/v1/images/generations",

View file

@ -178,6 +178,31 @@ def test_flux2_image_edit_preserves_controls_and_pixel_cost(dimensions: Mapping[
assert response._hidden_params["response_cost"] == pytest.approx(5e-08 * 2048 * 1024 * 2)
@pytest.mark.usefixtures("local_model_cost_map")
def test_flux2_image_edit_bills_the_reported_reference_megapixels() -> None:
metered_body: Final = {
"data": [{"b64_json": "aW1n"}],
"request_meta": {"cost": 999.0, "input_mp": 2.0, "output_mp": 1.0, "total_pixels": 1},
}
client: Final = HTTPHandler(
client=httpx.Client(transport=httpx.MockTransport(lambda _: httpx.Response(200, json=metered_body)))
)
response: Final = litellm.image_edit(
model="azure_ai/flux.2-pro",
image=[b"first", b"second"],
prompt="Combine them",
api_key="test-key",
api_base="https://example.services.ai.azure.com",
client=client,
)
prices: Final = litellm.model_cost["azure_ai/flux.2-pro"]
assert response._hidden_params["response_cost"] == pytest.approx(
prices["output_cost_per_first_megapixel"] + 2 * prices["input_cost_per_megapixel"]
)
def test_flux2_image_edit_accepts_and_drops_openai_only_parameters():
optional_params: Final = ImageEditRequestUtils.get_optional_params_image_edit(
model="FLUX.2-pro",

View file

@ -1,9 +1,11 @@
from collections.abc import Mapping
import json
from collections.abc import Callable, Mapping
from typing import Final
from unittest.mock import MagicMock
import httpx
import pytest
import respx
import litellm
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
@ -251,3 +253,229 @@ def test_flux2_response_preserves_mapped_dimensions():
encoding=None,
)
assert response.size == "2048x1024"
FOUNDRY_BASE: Final = "https://flux-billing.services.ai.azure.com"
def _flux2_route(provider_path: str) -> str:
return rf"https://flux-billing\.services\.ai\.azure\.com/providers/blackforestlabs/v1/{provider_path}\?.*"
def _metered_body(output_mp: float, input_mp: float, num_images: int = 1) -> Mapping[str, object]:
return {
"created": 1,
"data": [{"b64_json": "aW1n"} for _ in range(num_images)],
"request_meta": {"cost": 999.0, "input_mp": input_mp, "output_mp": output_mp, "total_pixels": 1},
}
def _price(model: str, key: str) -> float:
price: Final = litellm.model_cost[model][key]
assert isinstance(price, float) and price > 0
return price
# request_meta pairs Azure returned for FLUX.2 [pro] on 2026-10-27, whose credit charges matched whole megapixels
# rounded up: one 512x512 reference reported input_mp 0.25 and two of them reported 2.0
@pytest.mark.parametrize(
("output_mp", "input_mp", "additional_output_megapixels", "input_megapixels"),
[
(0.25, 0.0, 0, 0),
(1.5, 0.0, 1, 0),
(2.07, 0.0, 2, 0),
(1.0, 0.25, 0, 1),
(1.0, 1.5, 0, 2),
(1.0, 2.0, 0, 2),
],
)
@respx.mock
def test_flux2_pro_generation_bills_the_reported_megapixels_rounded_up(
output_mp: float, input_mp: float, additional_output_megapixels: int, input_megapixels: int
) -> None:
respx.post(url__regex=_flux2_route("flux-2-pro")).mock(
return_value=httpx.Response(200, json=_metered_body(output_mp, input_mp))
)
response: Final = litellm.image_generation(
model="azure_ai/flux.2-pro", prompt="A red fox", api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response._hidden_params["response_cost"] == pytest.approx(
_price("azure_ai/flux.2-pro", "output_cost_per_first_megapixel")
+ additional_output_megapixels * _price("azure_ai/flux.2-pro", "output_cost_per_megapixel")
+ input_megapixels * _price("azure_ai/flux.2-pro", "input_cost_per_megapixel")
)
@pytest.mark.parametrize(
("output_mp", "additional_output_megapixels"),
[(3.0, 1), (0.5, 0)],
)
@respx.mock
def test_flux2_pro_generation_bills_first_megapixel_per_returned_image(
output_mp: float, additional_output_megapixels: int
) -> None:
respx.post(url__regex=_flux2_route("flux-2-pro")).mock(
return_value=httpx.Response(200, json=_metered_body(output_mp, 0.25, num_images=2))
)
response: Final = litellm.image_generation(
model="azure_ai/flux.2-pro", prompt="A red fox", api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response._hidden_params["response_cost"] == pytest.approx(
2
* (
_price("azure_ai/flux.2-pro", "output_cost_per_first_megapixel")
+ additional_output_megapixels * _price("azure_ai/flux.2-pro", "output_cost_per_megapixel")
)
+ _price("azure_ai/flux.2-pro", "input_cost_per_megapixel")
)
@respx.mock
def test_flux2_pro_generation_without_returned_images_has_no_cost() -> None:
respx.post(url__regex=_flux2_route("flux-2-pro")).mock(
return_value=httpx.Response(200, json=_metered_body(3.0, 1.5, num_images=0))
)
response: Final = litellm.image_generation(
model="azure_ai/flux.2-pro", prompt="A red fox", n=4, api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response.data == []
assert response._hidden_params["response_cost"] == 0.0
@pytest.mark.asyncio
async def test_async_flux2_pro_generation_bills_the_reported_megapixels(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
respx_mock.post(url__regex=_flux2_route("flux-2-pro")).mock(
return_value=httpx.Response(200, json=_metered_body(1.5, 0.25))
)
response: Final = await litellm.aimage_generation(
model="azure_ai/flux.2-pro", prompt="A red fox", api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response._hidden_params["response_cost"] == pytest.approx(
_price("azure_ai/flux.2-pro", "output_cost_per_first_megapixel")
+ _price("azure_ai/flux.2-pro", "output_cost_per_megapixel")
+ _price("azure_ai/flux.2-pro", "input_cost_per_megapixel")
)
@respx.mock
def test_flux2_flex_generation_bills_every_reported_megapixel_at_one_rate() -> None:
respx.post(url__regex=_flux2_route("flux-2-flex")).mock(
return_value=httpx.Response(200, json=_metered_body(1.5, 0.25))
)
response: Final = litellm.image_generation(
model="azure_ai/FLUX.2-flex", prompt="A red fox", api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response._hidden_params["response_cost"] == pytest.approx(
2 * _price("azure_ai/FLUX.2-flex", "output_cost_per_megapixel")
+ _price("azure_ai/FLUX.2-flex", "input_cost_per_megapixel")
)
@pytest.mark.parametrize(
("deployment_prices", "expected_cost"),
[
(
{},
lambda price: (
price("output_cost_per_first_megapixel")
+ price("output_cost_per_megapixel")
+ price("input_cost_per_megapixel")
),
),
({"output_cost_per_image": 0.05}, lambda _: 0.05),
(
{"output_cost_per_megapixel": 0.02},
lambda price: price("output_cost_per_first_megapixel") + 0.02 + price("input_cost_per_megapixel"),
),
(
{
"output_cost_per_first_megapixel": 0.04,
"output_cost_per_megapixel": 0.02,
"input_cost_per_megapixel": 0.01,
},
lambda _: 0.04 + 0.02 + 0.01,
),
],
)
@respx.mock
def test_flux2_router_deployment_prices_layer_over_the_catalog_megapixel_prices(
deployment_prices: Mapping[str, float], expected_cost: Callable[[Callable[[str], float]], float]
) -> None:
respx.post(url__regex=_flux2_route("flux-2-pro")).mock(
return_value=httpx.Response(200, json=_metered_body(1.5, 0.25))
)
router: Final = litellm.Router(
model_list=[
{
"model_name": "flux-2-pro",
"litellm_params": {
"model": "azure_ai/flux.2-pro",
"api_base": FOUNDRY_BASE,
"api_key": "test-key",
**deployment_prices,
},
}
]
)
response: Final = router.image_generation(model="flux-2-pro", prompt="A red fox")
assert response._hidden_params["response_cost"] == pytest.approx(
expected_cost(lambda key: _price("azure_ai/flux.2-pro", key))
)
@pytest.mark.parametrize(
"request_meta",
[
None,
{"input_mp": 0.0, "output_mp": 0.0},
{"input_mp": -1.0, "output_mp": 1.0},
{"input_mp": 0.0, "output_mp": float("inf")},
{"input_mp": 0.0},
],
)
@respx.mock
def test_flux2_pro_generation_without_usable_megapixels_keeps_the_per_image_price(
request_meta: Mapping[str, float] | None,
) -> None:
body: Final = {"created": 1, "data": [{"b64_json": "aW1n"}], "request_meta": request_meta}
respx.post(url__regex=_flux2_route("flux-2-pro")).mock(
return_value=httpx.Response(200, content=json.dumps(body).encode())
)
response: Final = litellm.image_generation(
model="azure_ai/flux.2-pro", prompt="A red fox", api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response._hidden_params["response_cost"] == pytest.approx(
_price("azure_ai/flux.2-pro", "output_cost_per_image")
)
@respx.mock
def test_flux2_pro_generation_bills_no_references_when_input_megapixels_are_null() -> None:
body: Final = {"created": 1, "data": [{"b64_json": "aW1n"}], "request_meta": {"input_mp": None, "output_mp": 1.5}}
respx.post(url__regex=_flux2_route("flux-2-pro")).mock(return_value=httpx.Response(200, json=body))
response: Final = litellm.image_generation(
model="azure_ai/flux.2-pro", prompt="A red fox", api_base=FOUNDRY_BASE, api_key="test-key"
)
assert response._hidden_params["response_cost"] == pytest.approx(
_price("azure_ai/flux.2-pro", "output_cost_per_first_megapixel")
+ _price("azure_ai/flux.2-pro", "output_cost_per_megapixel")
)

View file

@ -1,5 +1,7 @@
import json
from collections.abc import Callable, Mapping
from datetime import datetime
from typing import Final
from unittest.mock import MagicMock
import httpx
@ -542,6 +544,41 @@ def test_flux_2_relay_through_the_provider_route_is_costed_per_image():
assert logging_obj.response_cost_calculator(result=result) == pytest.approx(per_image)
@pytest.mark.parametrize(
("model", "native_path", "catalog_key", "expected_cost"),
[
(
"FLUX.2-pro",
"providers/blackforestlabs/v1/flux-2-pro",
"azure_ai/flux.2-pro",
lambda prices: prices["output_cost_per_first_megapixel"]
+ prices["output_cost_per_megapixel"]
+ prices["input_cost_per_megapixel"],
),
(
"FLUX.2-flex",
"providers/blackforestlabs/v1/flux-2-flex",
"azure_ai/FLUX.2-flex",
lambda prices: 2 * prices["output_cost_per_megapixel"] + prices["input_cost_per_megapixel"],
),
],
)
@pytest.mark.usefixtures("local_model_cost_map")
def test_flux_2_relay_is_costed_from_the_reported_megapixels(
model: str, native_path: str, catalog_key: str, expected_cost: Callable[[Mapping[str, float]], float]
) -> None:
metered_body: Final = {
**IMAGE_BODY,
"request_meta": {"cost": 999.0, "input_mp": 0.25, "output_mp": 1.5, "total_pixels": 1},
}
result, logging_obj = _relay_logging_result(AzureAIPassthroughConfig(), model, native_path, metered_body)
assert logging_obj.call_type == "aimage_generation"
assert logging_obj.response_cost_calculator(result=result) == pytest.approx(
expected_cost(litellm.model_cost[catalog_key])
)
def test_rejected_rerank_relay_keeps_the_passthrough_object_and_call_type():
result, logging_obj = _relay_logging_result(
AzureAIPassthroughConfig(),

View file

@ -824,6 +824,9 @@ def validate_model_cost_values(model_data, exceptions=None):
"output_cost_per_image_4K",
"input_cost_per_pixel",
"output_cost_per_pixel",
"input_cost_per_megapixel",
"output_cost_per_first_megapixel",
"output_cost_per_megapixel",
"cost_per_second",
"input_cost_per_second",
"input_cost_per_text_message",
@ -1047,6 +1050,7 @@ _COST_MAP_ROW_SCHEMA: Final = {
"regional_endpoint_uplift_multiplier": {"type": "number"},
"regional_processing_uplift_multiplier_eu": {"type": "number"},
"regional_processing_uplift_multiplier_us": {"type": "number"},
"input_cost_per_megapixel": {"type": "number"},
"input_cost_per_pixel": {"type": "number"},
"input_cost_per_query": {"type": "number"},
"input_cost_per_request": {"type": "number"},
@ -1117,6 +1121,8 @@ _COST_MAP_ROW_SCHEMA: Final = {
"output_cost_per_image_token": {"type": "number"},
"output_cost_per_image_token_batches": {"type": "number"},
"output_cost_per_video_token": {"type": "number"},
"output_cost_per_first_megapixel": {"type": "number"},
"output_cost_per_megapixel": {"type": "number"},
"output_cost_per_pixel": {"type": "number"},
"output_cost_per_second": {"type": "number"},
"output_cost_per_second_480p": {"type": "number"},

View file

@ -35440,6 +35440,8 @@ export interface components {
input_cost_per_image_token?: number | null;
/** Input Cost Per Image Token Batches */
input_cost_per_image_token_batches?: number | null;
/** Input Cost Per Megapixel */
input_cost_per_megapixel?: number | null;
/** Input Cost Per Pixel */
input_cost_per_pixel?: number | null;
/** Input Cost Per Query */
@ -35555,6 +35557,8 @@ export interface components {
output_cost_per_character?: number | null;
/** Output Cost Per Character Above 128K Tokens */
output_cost_per_character_above_128k_tokens?: number | null;
/** Output Cost Per First Megapixel */
output_cost_per_first_megapixel?: number | null;
/** Output Cost Per Image */
output_cost_per_image?: number | null;
/** Output Cost Per Image 1024 */
@ -35567,6 +35571,8 @@ export interface components {
output_cost_per_image_token?: number | null;
/** Output Cost Per Image Token Batches */
output_cost_per_image_token_batches?: number | null;
/** Output Cost Per Megapixel */
output_cost_per_megapixel?: number | null;
/** Output Cost Per Pixel */
output_cost_per_pixel?: number | null;
/** Output Cost Per Reasoning Token */
@ -50838,6 +50844,8 @@ export interface components {
input_cost_per_image_token?: number | null;
/** Input Cost Per Image Token Batches */
input_cost_per_image_token_batches?: number | null;
/** Input Cost Per Megapixel */
input_cost_per_megapixel?: number | null;
/** Input Cost Per Pixel */
input_cost_per_pixel?: number | null;
/** Input Cost Per Query */
@ -50953,6 +50961,8 @@ export interface components {
output_cost_per_character?: number | null;
/** Output Cost Per Character Above 128K Tokens */
output_cost_per_character_above_128k_tokens?: number | null;
/** Output Cost Per First Megapixel */
output_cost_per_first_megapixel?: number | null;
/** Output Cost Per Image */
output_cost_per_image?: number | null;
/** Output Cost Per Image 1024 */
@ -50965,6 +50975,8 @@ export interface components {
output_cost_per_image_token?: number | null;
/** Output Cost Per Image Token Batches */
output_cost_per_image_token_batches?: number | null;
/** Output Cost Per Megapixel */
output_cost_per_megapixel?: number | null;
/** Output Cost Per Pixel */
output_cost_per_pixel?: number | null;
/** Output Cost Per Reasoning Token */