refactor(azure_ai): bill FLUX.2 references at the megapixel rate instead of a separate key

Azure's reference meter costs the same per megapixel as each additional
generated megapixel on both FLUX.2 models ($0.015 on Pro, $0.05 on Flex),
so input_cost_per_reference_pixel only duplicated input_cost_per_pixel.
References now bill at the price source's megapixel rate: the catalog
rate, a deployment's own input_cost_per_pixel, or nothing on top of a
flat output_cost_per_image. Billing is unchanged

Drop the key from the cost map, its schema, CustomPricingLiteLLMParams,
model info, the Rust catalog, the dashboard types and the Terraform
provider, along with the Terraform zero-rate update it needed. If Azure
ever prices the reference meter differently, the key can come back then
This commit is contained in:
Shreshth Kharbanda 2026-09-26 01:17:56 +00:00
parent babf5bc3b4
commit ba5b16226c
No known key found for this signature in database
17 changed files with 67 additions and 253 deletions

View file

@ -159,9 +159,6 @@ pub struct ModelInfo {
pub input_cost_per_pixel: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_cost_per_query: Option<f64>,
/// USD per billable pixel of the reference images sent with an image edit (Azure AI FLUX.2 counts them in whole 1024x1024 megapixels).
#[serde(skip_serializing_if = "Option::is_none")]
pub input_cost_per_reference_pixel: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_cost_per_request: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]

View file

@ -26,8 +26,7 @@ _REFERENCE_PIXELS: Final = TypeAdapter(tuple[Annotated[int, Field(strict=True, g
@dataclass(frozen=True, slots=True, kw_only=True)
class _Flux2MegapixelPrices:
first: float
additional: float
reference: float
megapixel: float
def _price(resolved: ModelInfo, cost_key: str) -> float | None:
@ -52,30 +51,19 @@ def _deployment_price(deployment: ModelInfo | None, cost_key: str) -> float | No
def _flux2_prices(resolved: ModelInfo, deployment: ModelInfo | None) -> _Flux2MegapixelPrices:
# A deployment that prices the generated image itself also prices its references, unless it sets a reference
# rate: a flat per-image price covers them, and a pixel rate bills them at that rate
deployment_reference_rate: Final = _deployment_price(deployment, "input_cost_per_reference_pixel")
deployment_image_price: Final = _deployment_price(deployment, "output_cost_per_image")
if deployment_image_price is not None:
return _Flux2MegapixelPrices(
first=deployment_image_price,
additional=0.0,
reference=(deployment_reference_rate or 0.0) * MEGAPIXEL,
)
return _Flux2MegapixelPrices(first=deployment_image_price, megapixel=0.0)
deployment_pixel_rate: Final = _deployment_price(deployment, "input_cost_per_pixel")
if deployment_pixel_rate is not None:
return _Flux2MegapixelPrices(
first=deployment_pixel_rate * MEGAPIXEL,
additional=deployment_pixel_rate * MEGAPIXEL,
reference=(deployment_pixel_rate if deployment_reference_rate is None else deployment_reference_rate)
* MEGAPIXEL,
first=deployment_pixel_rate * MEGAPIXEL, megapixel=deployment_pixel_rate * MEGAPIXEL
)
catalog_megapixel_rate: Final = _pixel_rate(resolved, "input_cost_per_pixel") * MEGAPIXEL
catalog_first_megapixel: Final = _price(resolved, "output_cost_per_image")
return _Flux2MegapixelPrices(
first=catalog_megapixel_rate if catalog_first_megapixel is None else catalog_first_megapixel,
additional=catalog_megapixel_rate,
reference=_pixel_rate(resolved, "input_cost_per_reference_pixel") * MEGAPIXEL,
megapixel=catalog_megapixel_rate,
)
@ -84,8 +72,9 @@ def _billable_megapixels(pixels: int) -> int:
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
# Azure's FLUX.2 request_meta (2026-09-25) bills a lone reference at no more than 4 MP, each reference of a
# multi-reference edit as exactly 1 MP whatever its size, and every reference megapixel at the rate of an
# additional generated one
match reference_pixels:
case ():
return 0
@ -104,14 +93,14 @@ def _reference_cost(prices: _Flux2MegapixelPrices, image_response: ImageResponse
except ValidationError:
verbose_logger.warning("Ignoring malformed FLUX.2 reference pixel counts: %r", reported_pixels)
return 0.0
return prices.reference * _billable_reference_megapixels(reference_pixels)
return prices.megapixel * _billable_reference_megapixels(reference_pixels)
def _flux2_generated_cost(
prices: _Flux2MegapixelPrices, image_response: ImageResponse, requested_pixels: int, n: int | None
) -> float:
return sum(
prices.first + prices.additional * (_billable_megapixels(pixels) - 1)
prices.first + prices.megapixel * (_billable_megapixels(pixels) - 1)
for pixels in _generated_pixels(image_response, requested_pixels, n)
)

View file

@ -11372,7 +11372,6 @@
},
"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.03,
@ -11391,7 +11390,6 @@
},
"azure_ai/FLUX.2-flex": {
"input_cost_per_pixel": 4.76837158203125e-08,
"input_cost_per_reference_pixel": 4.76837158203125e-08,
"litellm_provider": "azure_ai",
"max_input_tokens": 32000,
"max_tokens": 32000,

View file

@ -789,7 +789,6 @@ class ModelGroupInfo(BaseModel):
input_cost_per_token: float | None = None
output_cost_per_token: float | None = None
input_cost_per_pixel: float | None = None
input_cost_per_reference_pixel: float | None = None
mode: (
str
| Literal["chat", "embedding", "completion", "image_generation", "audio_transcription", "rerank", "moderations"]

View file

@ -358,7 +358,6 @@ 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]
input_cost_per_reference_pixel: ReadOnly[float | None]
output_cost_per_image_token: float | None
output_cost_per_video_token: float | None # for gemini omni models with video output
output_vector_size: int | None
@ -3709,7 +3708,6 @@ class CustomPricingLiteLLMParams(MirroredPricingParams):
output_cost_per_image_1024: float | None = None
output_cost_per_image_1536: float | None = None
input_cost_per_pixel: float | None = None
input_cost_per_reference_pixel: float | None = None
output_cost_per_pixel: float | None = None
# Include all ModelInfoBase fields as optional

View file

@ -6265,7 +6265,6 @@ 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),
input_cost_per_reference_pixel=_model_info.get("input_cost_per_reference_pixel", None),
output_cost_per_image_token=_model_info.get("output_cost_per_image_token", None),
output_cost_per_video_token=_model_info.get("output_cost_per_video_token", None),
output_vector_size=_model_info.get("output_vector_size", None),

View file

@ -11372,7 +11372,6 @@
},
"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.03,
@ -11391,7 +11390,6 @@
},
"azure_ai/FLUX.2-flex": {
"input_cost_per_pixel": 4.76837158203125e-08,
"input_cost_per_reference_pixel": 4.76837158203125e-08,
"litellm_provider": "azure_ai",
"max_input_tokens": 32000,
"max_tokens": 32000,

View file

@ -333,10 +333,6 @@
"type": "number",
"minimum": 0
},
"input_cost_per_reference_pixel": {
"type": "number",
"minimum": 0
},
"input_cost_per_request": {
"type": "number",
"minimum": 0

View file

@ -159,8 +159,6 @@ The following arguments are supported:
* `input_cost_per_pixel` - (Optional) float. Cost applied per input pixel for models that charge by image size.
* `input_cost_per_reference_pixel` - (Optional) float. Cost applied per reference image pixel for image-edit models.
* `output_cost_per_pixel` - (Optional) float. Cost applied per output pixel for image-generation models.
* `input_cost_per_second` - (Optional) float. Cost applied per input second for audio/transcription models.

View file

@ -124,10 +124,6 @@ func resourceLiteLLMModel() *schema.Resource {
Type: schema.TypeFloat,
Optional: true,
},
"input_cost_per_reference_pixel": {
Type: schema.TypeFloat,
Optional: true,
},
"output_cost_per_pixel": {
Type: schema.TypeFloat,
Optional: true,

View file

@ -126,10 +126,6 @@ func createOrUpdateModel(d *schema.ResourceData, m interface{}, isUpdate bool) e
if inputCostPerPixel := d.Get("input_cost_per_pixel").(float64); inputCostPerPixel > 0 {
litellmParams["input_cost_per_pixel"] = inputCostPerPixel
}
inputCostPerReferencePixel := d.Get("input_cost_per_reference_pixel").(float64)
if inputCostPerReferencePixel > 0 || (isUpdate && d.HasChange("input_cost_per_reference_pixel")) {
litellmParams["input_cost_per_reference_pixel"] = inputCostPerReferencePixel
}
if outputCostPerPixel := d.Get("output_cost_per_pixel").(float64); outputCostPerPixel > 0 {
litellmParams["output_cost_per_pixel"] = outputCostPerPixel
}

View file

@ -242,71 +242,3 @@ func TestResourceLiteLLMModelUpdateSkipsPatchWhenDisplayNameUnchanged(t *testing
t.Fatalf("update failed: %v", err)
}
}
func TestResourceLiteLLMModelUpdateSendsReferencePixelRate(t *testing.T) {
cases := map[string]struct {
oldRate, newRate string
wantSent bool
wantRate float64
}{
"rate cleared to zero is sent": {oldRate: "2e-07", newRate: "0", wantSent: true, wantRate: 0},
"unchanged rate is sent": {oldRate: "2e-07", newRate: "2e-07", wantSent: true, wantRate: 2e-07},
"never set rate is omitted": {oldRate: "", newRate: "0", wantSent: false},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
var updateParams map[string]interface{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.URL.Path == "/model/update" {
var payload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Errorf("failed to decode update payload: %v", err)
}
updateParams = payload["litellm_params"].(map[string]interface{})
w.Write([]byte(modelInfoBody("")))
return
}
w.Write([]byte(modelInfoDataEnvelope("")))
}))
defer srv.Close()
res := resourceLiteLLMModel()
attrs := map[string]string{
"model_name": "sonnet-4-5-anthropic",
"custom_llm_provider": "anthropic",
"base_model": "claude-sonnet-4-5",
}
if tc.oldRate != "" {
attrs["input_cost_per_reference_pixel"] = tc.oldRate
}
state := &terraform.InstanceState{ID: "model-123", Attributes: attrs}
config := map[string]interface{}{
"model_name": "sonnet-4-5-anthropic",
"custom_llm_provider": "anthropic",
"base_model": "claude-sonnet-4-5",
}
if tc.newRate != "0" {
config["input_cost_per_reference_pixel"] = tc.newRate
}
diff, err := res.Diff(context.Background(), state, &terraform.ResourceConfig{Config: config}, nil)
if err != nil {
t.Fatalf("diff failed: %v", err)
}
d, err := schema.InternalMap(res.Schema).Data(state, diff)
if err != nil {
t.Fatalf("data failed: %v", err)
}
if err := resourceLiteLLMModelUpdate(d, NewClient(srv.URL, "test-key", true)); err != nil {
t.Fatalf("update failed: %v", err)
}
rate, sent := updateParams["input_cost_per_reference_pixel"]
if sent != tc.wantSent {
t.Fatalf("expected input_cost_per_reference_pixel sent=%v, got %v (%v)", tc.wantSent, sent, rate)
}
if sent && rate != tc.wantRate {
t.Errorf("expected input_cost_per_reference_pixel %v, got %v", tc.wantRate, rate)
}
})
}
}

View file

@ -103,7 +103,6 @@ type LiteLLMParams struct {
InputCostPerToken float64 `json:"input_cost_per_token,omitempty"`
OutputCostPerToken float64 `json:"output_cost_per_token,omitempty"`
InputCostPerPixel float64 `json:"input_cost_per_pixel,omitempty"`
InputCostPerReferencePixel float64 `json:"input_cost_per_reference_pixel,omitempty"`
OutputCostPerPixel float64 `json:"output_cost_per_pixel,omitempty"`
InputCostPerSecond float64 `json:"input_cost_per_second,omitempty"`
OutputCostPerSecond float64 `json:"output_cost_per_second,omitempty"`

View file

@ -184,11 +184,9 @@ def test_flux2_image_edit_preserves_controls_and_pixel_cost(dimensions: Mapping[
steps="32",
**dimensions,
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
assert response._hidden_params["response_cost"] == pytest.approx(
generated_rate * 2048 * 1024 * 2 + reference_rate * 1024 * 1024
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 2048 * 1024 * 2 + rate * 1024 * 1024)
def test_flux2_image_edit_accepts_and_drops_openai_only_parameters():
@ -225,9 +223,8 @@ def _webp(width: int, height: int) -> bytes:
)
def _flex_rates() -> tuple[float, float]:
row: Final = litellm.model_cost["azure_ai/FLUX.2-flex"]
return row["input_cost_per_pixel"], row["input_cost_per_reference_pixel"]
def _flex_megapixel_rate() -> float:
return litellm.model_cost["azure_ai/FLUX.2-flex"]["input_cost_per_pixel"]
def _edit_ok(request: httpx.Request) -> httpx.Response:
@ -251,14 +248,12 @@ def test_flux2_image_edit_measures_every_reference_but_bills_each_of_several_as_
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(respond))),
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
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"] == (1024 * 1024, 800 * 600, 640 * 480)
assert response._hidden_params["response_cost"] == pytest.approx(
generated_rate * 1024 * 1024 + reference_rate * 3 * 1024 * 1024
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 1024 * 1024 + rate * 3 * 1024 * 1024)
class _ReadOnlyUpload:
@ -353,12 +348,10 @@ def test_flux2_image_edit_sends_and_bills_every_readable_upload(
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(respond))),
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
assert sent_images == [base64.b64encode(reference).decode()]
assert response._hidden_params["response_cost"] == pytest.approx(
generated_rate * 1024 * 1024 + reference_rate * 2 * 1024 * 1024
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 1024 * 1024 + rate * 2 * 1024 * 1024)
@pytest.mark.parametrize("upload_kind", ("bytesio", "buffered-reader", "named-temporary-file-at-eof"))
@ -420,12 +413,10 @@ def test_flux2_image_edit_measures_a_stream_reference():
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
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
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 1024 * 1024 + rate * 2048 * 2048)
# Billable megapixels for a lone reference as Azure's FLUX.2-pro request_meta reported them on 2026-09-25
@ -447,10 +438,10 @@ def test_flux2_image_edit_bills_a_lone_reference_in_whole_megapixels(reference:
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
assert response._hidden_params["response_cost"] == pytest.approx(
generated_rate * 1024 * 1024 + reference_rate * billed_megapixels * 1024 * 1024
rate * 1024 * 1024 + rate * billed_megapixels * 1024 * 1024
)
@ -472,12 +463,10 @@ def test_flux2_image_edit_bills_a_lone_unmeasurable_reference_as_one_megapixel(r
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
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
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 1024 * 1024 + rate * 1024 * 1024)
def test_flux2_image_edit_still_bills_every_reference_when_one_header_reports_zero_pixels():
@ -490,11 +479,9 @@ def test_flux2_image_edit_still_bills_every_reference_when_one_header_reports_ze
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(_edit_ok))),
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
assert response._hidden_params["response_cost"] == pytest.approx(
generated_rate * 1024 * 1024 + reference_rate * 2 * 1024 * 1024
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 1024 * 1024 + rate * 2 * 1024 * 1024)
@pytest.mark.parametrize("stream_position", ("start", "end"))
@ -526,7 +513,7 @@ def test_flux2_image_edit_resends_and_rebills_a_reused_stream(stream_position: s
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():
def test_flux2_pro_image_edit_bills_references_at_the_pro_megapixel_rate():
pro_row: Final = litellm.model_cost["azure_ai/flux.2-pro"]
response: Final = litellm.image_edit(
model="azure_ai/flux.2-pro",
@ -538,9 +525,9 @@ def test_flux2_pro_image_edit_bills_references_on_the_pro_reference_rate():
size="1024x1024",
)
assert pro_row["input_cost_per_reference_pixel"] > 0
assert pro_row["input_cost_per_pixel"] > 0
assert response._hidden_params["response_cost"] == pytest.approx(
pro_row["output_cost_per_image"] + pro_row["input_cost_per_reference_pixel"] * 2 * 1024 * 1024
pro_row["output_cost_per_image"] + pro_row["input_cost_per_pixel"] * 2 * 1024 * 1024
)
@ -548,8 +535,7 @@ async def test_flux2_router_image_edit_bills_the_deployment_rates(monkeypatch: p
mock_client: Final = AsyncHTTPHandler()
mock_client.client = httpx.AsyncClient(transport=httpx.MockTransport(_edit_ok))
monkeypatch.setattr(llm_http_handler_module, "get_async_httpx_client", lambda **_kwargs: mock_client)
generated_rate: Final = 1e-07
reference_rate: Final = 2e-07
megapixel_rate: Final = 1e-07
router: Final = litellm.Router(
model_list=[
{
@ -558,8 +544,7 @@ async def test_flux2_router_image_edit_bills_the_deployment_rates(monkeypatch: p
"model": "azure_ai/FLUX.2-flex",
"api_base": "https://example.services.ai.azure.com",
"api_key": "test-key",
"input_cost_per_pixel": generated_rate,
"input_cost_per_reference_pixel": reference_rate,
"input_cost_per_pixel": megapixel_rate,
},
}
]
@ -573,7 +558,7 @@ async def test_flux2_router_image_edit_bills_the_deployment_rates(monkeypatch: p
)
assert response._hidden_params["response_cost"] == pytest.approx(
generated_rate * 2 * 1024 * 1024 + reference_rate * 2 * 1024 * 1024
megapixel_rate * 2 * 1024 * 1024 + megapixel_rate * 2 * 1024 * 1024
)
@ -582,8 +567,8 @@ async def test_flux2_router_image_edit_bills_the_deployment_rates(monkeypatch: p
(
({"output_cost_per_image": 0.5}, 0.5),
(
{"input_cost_per_pixel": 1e-07, "input_cost_per_reference_pixel": 2e-07},
1e-07 * 2 * 1024 * 1024 + 2e-07 * 2 * 1024 * 1024,
{"input_cost_per_pixel": 1e-07},
1e-07 * 2 * 1024 * 1024 + 1e-07 * 2 * 1024 * 1024,
),
),
ids=("flat", "per-pixel"),
@ -663,7 +648,7 @@ def test_flux2_image_edit_bills_the_generated_image_azure_returned(size: str | N
assert response._hidden_params["response_cost"] == pytest.approx(
pro_row["output_cost_per_image"]
+ pro_row["input_cost_per_pixel"] * 1024 * 1024
+ pro_row["input_cost_per_reference_pixel"] * 2 * 1024 * 1024
+ pro_row["input_cost_per_pixel"] * 2 * 1024 * 1024
)
@ -679,9 +664,7 @@ async def test_flux2_aimage_edit_bills_references_like_image_edit():
client=client,
size="1024x1024",
)
generated_rate, reference_rate = _flex_rates()
rate: Final = _flex_megapixel_rate()
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
)
assert response._hidden_params["response_cost"] == pytest.approx(rate * 1024 * 1024 + rate * 2 * 1024 * 1024)

View file

@ -35,10 +35,6 @@ def _flex_pixel_rate() -> float:
return litellm.model_cost["azure_ai/FLUX.2-flex"]["input_cost_per_pixel"]
def _flex_reference_rate() -> float:
return litellm.model_cost["azure_ai/FLUX.2-flex"]["input_cost_per_reference_pixel"]
def _flex_edit_cost(response: ImageResponse, model_info: dict | None = None) -> float:
return CostCalculatorUtils.route_image_generation_cost_calculator(
model="FLUX.2-flex",
@ -58,13 +54,9 @@ def _edit_response(reference_image_pixels: object) -> ImageResponse:
)
def _pro_megapixel_rates() -> tuple[float, float, float]:
def _pro_megapixel_rates() -> tuple[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,
)
return row["output_cost_per_image"], row["input_cost_per_pixel"] * 1024 * 1024
@pytest.mark.parametrize(
@ -171,13 +163,9 @@ def test_flux2_flex_model_info():
@pytest.mark.parametrize("model", ("flux.2-pro", "FLUX.2-flex"))
def test_flux2_model_info_reports_the_reference_rate_and_the_edit_endpoint(model: str):
def test_flux2_model_info_lists_the_edit_endpoint(model: str):
model_info: Final = litellm.get_model_info(model=model, custom_llm_provider="azure_ai")
assert (
model_info["input_cost_per_reference_pixel"]
== litellm.model_cost[f"azure_ai/{model}"]["input_cost_per_reference_pixel"]
)
assert "/v1/images/edits" in model_info["supported_endpoints"]
@ -269,7 +257,7 @@ def test_flux2_flex_generation_rounds_each_image_up_to_whole_megapixels():
@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()
first, additional = _pro_megapixel_rates()
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="flux.2-pro",
@ -305,9 +293,6 @@ def _catalog_image_cost(model: str, megapixels: int) -> float:
lambda megapixels: 1e-07 * 1024 * 1024 * megapixels,
id="deployment-pixel-rate-prices-every-megapixel",
),
pytest.param(
{"input_cost_per_reference_pixel": 3e-07}, None, id="deployment-reference-rate-keeps-catalog-output"
),
),
)
def test_flux2_generation_bills_each_price_source_as_its_owner_set_it(
@ -333,24 +318,16 @@ def test_flux2_generation_bills_each_price_source_as_its_owner_set_it(
@pytest.mark.parametrize(
("deployment_prices", "reference_megapixel_price"),
(
pytest.param(None, None, id="catalog-reference-rate"),
pytest.param(None, None, id="catalog-megapixel-rate"),
pytest.param({"output_cost_per_image": 0.07}, 0.0, id="deployment-flat-price-covers-references"),
pytest.param(
{"output_cost_per_image": 0.07, "input_cost_per_reference_pixel": 3e-07},
3e-07 * 1024 * 1024,
id="deployment-flat-price-with-its-reference-rate",
{"output_cost_per_image": 0.07, "input_cost_per_pixel": 1e-07},
0.0,
id="deployment-flat-price-covers-references-despite-its-pixel-rate",
),
pytest.param(
{"input_cost_per_pixel": 1e-07}, 1e-07 * 1024 * 1024, id="deployment-pixel-rate-prices-references"
),
pytest.param(
{"input_cost_per_pixel": 1e-07, "input_cost_per_reference_pixel": 3e-07},
3e-07 * 1024 * 1024,
id="deployment-reference-rate-wins-over-its-pixel-rate",
),
pytest.param(
{"input_cost_per_reference_pixel": 3e-07}, 3e-07 * 1024 * 1024, id="deployment-reference-rate-alone"
),
),
)
def test_flux2_edit_prices_references_from_the_source_that_priced_the_image(
@ -367,7 +344,7 @@ def test_flux2_edit_prices_references_from_the_source_that_priced_the_image(
)
expected_per_megapixel: Final = (
litellm.model_cost[f"azure_ai/{model}"]["input_cost_per_reference_pixel"] * 1024 * 1024
litellm.model_cost[f"azure_ai/{model}"]["input_cost_per_pixel"] * 1024 * 1024
if reference_megapixel_price is None
else reference_megapixel_price
)
@ -397,7 +374,7 @@ def _png_b64(width: int, height: int) -> str:
def test_flux2_pro_bills_each_generated_image_by_its_returned_size(
returned_images: tuple[tuple[int, int] | None, ...], requested_size: str, expected_megapixels: tuple[int, ...]
):
first, additional, _reference = _pro_megapixel_rates()
first, additional = _pro_megapixel_rates()
response: Final = ImageResponse(
data=[ImageObject(b64_json="aW1n" if image is None else _png_b64(*image)) for image in returned_images]
)
@ -415,7 +392,7 @@ def test_flux2_pro_bills_each_generated_image_by_its_returned_size(
@pytest.mark.parametrize("size", ("auto", "large", "0x1024", "1024x"))
def test_flux2_pro_bills_one_megapixel_for_a_size_it_cannot_measure(size: str):
first, _additional, _reference = _pro_megapixel_rates()
first, _additional = _pro_megapixel_rates()
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="flux.2-pro",
@ -429,7 +406,7 @@ def test_flux2_pro_bills_one_megapixel_for_a_size_it_cannot_measure(size: str):
def test_flux2_pro_bills_the_requested_size_when_the_returned_image_is_not_valid_base64():
first, additional, _reference = _pro_megapixel_rates()
first, additional = _pro_megapixel_rates()
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="flux.2-pro",
@ -444,7 +421,7 @@ def test_flux2_pro_bills_the_requested_size_when_the_returned_image_is_not_valid
@pytest.mark.parametrize(("n", "billed_images"), ((2, 2), (None, 0)))
def test_flux2_pro_bills_the_requested_image_count_when_the_response_lists_none(n: int | None, billed_images: int):
first, _additional, _reference = _pro_megapixel_rates()
first, _additional = _pro_megapixel_rates()
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="flux.2-pro",
@ -459,7 +436,7 @@ def test_flux2_pro_bills_the_requested_image_count_when_the_response_lists_none(
def test_flux2_pro_reads_an_uppercase_size():
first, additional, _reference = _pro_megapixel_rates()
first, additional = _pro_megapixel_rates()
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="flux.2-pro",
@ -475,22 +452,20 @@ def test_flux2_pro_reads_an_uppercase_size():
def test_flux2_edit_parses_string_prices_registered_in_the_cost_map(monkeypatch: pytest.MonkeyPatch):
row: Final = litellm.model_cost["azure_ai/FLUX.2-flex"]
monkeypatch.setitem(row, "input_cost_per_pixel", "2e-07")
monkeypatch.setitem(row, "input_cost_per_reference_pixel", "3e-07")
litellm.get_model_info.cache_clear()
_invalidate_model_cost_lowercase_map()
assert _flex_edit_cost(_edit_response((1024 * 1024,))) == pytest.approx(2e-07 * 1024 * 1024 + 3e-07 * 1024 * 1024)
assert _flex_edit_cost(_edit_response((1024 * 1024,))) == pytest.approx(2e-07 * 1024 * 1024 * 2)
@pytest.fixture
def distinct_pro_megapixel_prices(monkeypatch: pytest.MonkeyPatch) -> tuple[float, float, float]:
def distinct_pro_megapixel_prices(monkeypatch: pytest.MonkeyPatch) -> tuple[float, float]:
row: Final = litellm.model_cost["azure_ai/flux.2-pro"]
monkeypatch.setitem(row, "output_cost_per_image", 0.05)
monkeypatch.setitem(row, "input_cost_per_pixel", 2e-08)
monkeypatch.setitem(row, "input_cost_per_reference_pixel", 3e-08)
litellm.get_model_info.cache_clear()
_invalidate_model_cost_lowercase_map()
return 0.05, 2e-08 * 1024 * 1024, 3e-08 * 1024 * 1024
return 0.05, 2e-08 * 1024 * 1024
# Billable megapixels as Azure's request_meta reported them for FLUX.2-pro edits on 2026-09-25
@ -511,9 +486,9 @@ def test_flux2_pro_edit_bills_the_megapixels_azure_meters(
reference_pixels: tuple[int, ...],
output_megapixels: int,
reference_megapixels: int,
distinct_pro_megapixel_prices: tuple[float, float, float],
distinct_pro_megapixel_prices: tuple[float, float],
):
first, additional, reference = distinct_pro_megapixel_prices
first, megapixel = distinct_pro_megapixel_prices
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="flux.2-pro",
@ -523,7 +498,7 @@ def test_flux2_pro_edit_bills_the_megapixels_azure_meters(
call_type="image_edit",
)
assert cost == pytest.approx(first + additional * (output_megapixels - 1) + reference * reference_megapixels)
assert cost == pytest.approx(first + megapixel * (output_megapixels - 1 + reference_megapixels))
def test_flux2_flex_edit_bills_reference_pixels_on_top_of_generated_pixels():
@ -531,10 +506,10 @@ def test_flux2_flex_edit_bills_reference_pixels_on_top_of_generated_pixels():
assert generated_only == pytest.approx(_flex_pixel_rate() * 1024 * 1024)
assert _flex_edit_cost(_edit_response((1024 * 1024,))) - generated_only == pytest.approx(
_flex_reference_rate() * 1024 * 1024
_flex_pixel_rate() * 1024 * 1024
)
assert _flex_edit_cost(_edit_response((3 * 1024 * 1024,))) - generated_only == pytest.approx(
_flex_reference_rate() * 3 * 1024 * 1024
_flex_pixel_rate() * 3 * 1024 * 1024
)
@ -546,7 +521,7 @@ def test_flux2_flex_edit_reference_cost_does_not_scale_with_image_count():
)
assert _flex_edit_cost(two_outputs) == pytest.approx(
_flex_pixel_rate() * 1024 * 1024 * 2 + _flex_reference_rate() * 2048 * 1024
_flex_pixel_rate() * 1024 * 1024 * 2 + _flex_pixel_rate() * 2048 * 1024
)
@ -582,16 +557,6 @@ def test_flux2_flex_edit_ignores_reference_pixels_when_provider_reports_token_us
assert _flex_edit_cost(response, model_info=token_rates) == pytest.approx(50 * 1e-05 + 100 * 2e-05 + 1000 * 4e-05)
def test_flux2_flex_edit_reads_the_reference_rate_from_its_own_catalog_key(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setitem(litellm.model_cost["azure_ai/FLUX.2-flex"], "input_cost_per_reference_pixel", 3e-07)
litellm.get_model_info.cache_clear()
_invalidate_model_cost_lowercase_map()
assert _flex_edit_cost(_edit_response((1024 * 1024,))) == pytest.approx(
_flex_pixel_rate() * 1024 * 1024 + 3e-07 * 1024 * 1024
)
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")],
@ -600,7 +565,7 @@ def test_flux2_flex_edit_prices_output_from_the_request_size_not_the_reference()
)
assert _flex_edit_cost(large_reference_small_output) == pytest.approx(
_flex_pixel_rate() * 1024 * 1024 + _flex_reference_rate() * 2048 * 2048
_flex_pixel_rate() * 1024 * 1024 + _flex_pixel_rate() * 2048 * 2048
)
@ -610,51 +575,30 @@ def test_flux2_flex_edit_with_a_free_deployment_pixel_rate_bills_nothing():
assert cost == 0.0
def test_flux2_flex_edit_prefers_deployment_reference_rate():
cost: Final = _flex_edit_cost(
_edit_response((1024 * 1024,)),
model_info={"input_cost_per_pixel": 2e-07, "input_cost_per_reference_pixel": 3e-07},
)
assert cost == pytest.approx(2e-07 * 1024 * 1024 + 3e-07 * 1024 * 1024)
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})
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})
assert cost == pytest.approx(_flex_pixel_rate() * 1024 * 1024)
def test_custom_named_flux2_deployment_bills_its_own_megapixel_and_reference_rates() -> None:
def test_custom_named_flux2_deployment_bills_its_own_megapixel_rate_for_output_and_references() -> None:
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="my-flux2-prod",
completion_response=_edit_response((1024 * 1280,)),
custom_llm_provider="azure_ai",
size="1024x1280",
call_type="image_edit",
model_info={"input_cost_per_pixel": 1e-07, "input_cost_per_reference_pixel": 2e-07},
model_info={"input_cost_per_pixel": 1e-07},
)
assert cost == pytest.approx(1e-07 * 1024 * 1024 * 2 + 2e-07 * 1024 * 1024 * 2)
assert cost == pytest.approx(1e-07 * 1024 * 1024 * 2 + 1e-07 * 1024 * 1024 * 2)
def test_custom_named_flux2_deployment_with_only_a_reference_rate_bills_only_its_references() -> None:
def test_custom_named_flux2_deployment_without_an_image_price_bills_nothing() -> None:
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="my-flux2-prod",
completion_response=_edit_response((1024 * 1280,)),
custom_llm_provider="azure_ai",
size="1024x1280",
call_type="image_edit",
model_info={"input_cost_per_reference_pixel": 2e-07},
model_info={"input_cost_per_second": 1.0},
)
assert cost == pytest.approx(2e-07 * 1024 * 1024 * 2)
assert cost == 0.0
def test_azure_ai_image_cost_calculator_rejects_a_response_that_is_not_an_image_response() -> None:
@ -712,7 +656,7 @@ def test_unlisted_azure_ai_deployment_without_a_generated_image_price_bills_noth
custom_llm_provider="azure_ai",
size="1024x1024",
call_type="image_generation",
model_info={"input_cost_per_reference_pixel": 1e-07},
model_info={"input_cost_per_second": 1.0},
)
assert cost == 0.0

View file

@ -647,7 +647,6 @@ def validate_model_cost_values(model_data, exceptions=None):
"output_cost_per_image_2K",
"output_cost_per_image_4K",
"input_cost_per_pixel",
"input_cost_per_reference_pixel",
"output_cost_per_pixel",
"input_cost_per_second",
"output_cost_per_second",
@ -824,7 +823,6 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"regional_processing_uplift_multiplier_eu": {"type": "number"},
"regional_processing_uplift_multiplier_us": {"type": "number"},
"input_cost_per_pixel": {"type": "number"},
"input_cost_per_reference_pixel": {"type": "number"},
"input_cost_per_query": {"type": "number"},
"input_cost_per_request": {"type": "number"},
"input_cost_per_second": {"type": "number"},

View file

@ -33053,8 +33053,6 @@ export interface components {
input_cost_per_pixel?: number | null;
/** Input Cost Per Query */
input_cost_per_query?: number | null;
/** Input Cost Per Reference Pixel */
input_cost_per_reference_pixel?: number | null;
/** Input Cost Per Second */
input_cost_per_second?: number | null;
/** Input Cost Per Token */
@ -35942,8 +35940,6 @@ export interface components {
health_status?: string | null;
/** Input Cost Per Pixel */
input_cost_per_pixel?: number | null;
/** Input Cost Per Reference Pixel */
input_cost_per_reference_pixel?: number | null;
/** Input Cost Per Token */
input_cost_per_token?: number | null;
/**
@ -46850,8 +46846,6 @@ export interface components {
input_cost_per_pixel?: number | null;
/** Input Cost Per Query */
input_cost_per_query?: number | null;
/** Input Cost Per Reference Pixel */
input_cost_per_reference_pixel?: number | null;
/** Input Cost Per Second */
input_cost_per_second?: number | null;
/** Input Cost Per Token */