mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
Merge d3ee156ad0 into a66dc50efb
This commit is contained in:
commit
7b3d261cf3
17 changed files with 453 additions and 9 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue