mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
feat(fal_ai): add Nano Banana 2.1, bill it from fal billable units, fix google/ routing (#51410)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
00bd56760d
commit
b60df93c46
13 changed files with 280 additions and 13 deletions
|
|
@ -1,12 +1,13 @@
|
|||
import os
|
||||
from collections.abc import Mapping
|
||||
from math import ceil
|
||||
from math import ceil, isfinite
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.core_helpers import get_provider_response_headers_from_hidden_params
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import deployment_pricing, resolve_image_model_info
|
||||
from litellm.types.utils import ImageObject, ImageResponse, ModelInfo
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ FAL_NAMED_IMAGE_SIZES: Final[Mapping[str, str]] = MappingProxyType(
|
|||
_OBJECT_MAP: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object])
|
||||
|
||||
FAL_AI_QUEUE_DEFAULT_BASE: Final[str] = "https://queue.fal.run"
|
||||
FAL_BILLABLE_UNITS_HEADER: Final[str] = "x-fal-billable-units"
|
||||
|
||||
|
||||
def fal_ai_queue_base() -> str:
|
||||
|
|
@ -150,6 +152,26 @@ def _resolution_cost_per_image(entry: Mapping[str, object] | None, resolution: o
|
|||
return float(cost) if isinstance(cost, (int, float)) else None
|
||||
|
||||
|
||||
def _billable_units(response: object) -> float | None:
|
||||
headers: Final = get_provider_response_headers_from_hidden_params(response)
|
||||
raw_units: Final = None if headers is None else headers.get(FAL_BILLABLE_UNITS_HEADER)
|
||||
if raw_units is None:
|
||||
return None
|
||||
try:
|
||||
units: Final = float(raw_units)
|
||||
except ValueError:
|
||||
return None
|
||||
return units if isfinite(units) and units >= 0 else None
|
||||
|
||||
|
||||
def _billable_units_cost(prices: Mapping[str, object] | None, response: object) -> float | None:
|
||||
unit_cost: Final = None if prices is None else prices.get("output_cost_per_billable_unit")
|
||||
units: Final = _billable_units(response)
|
||||
if isinstance(unit_cost, bool) or not isinstance(unit_cost, (int, float)) or units is None:
|
||||
return None
|
||||
return unit_cost * units
|
||||
|
||||
|
||||
def _requested_image_count(request_body: Mapping[str, object]) -> int:
|
||||
num_images: Final = request_body.get("num_images")
|
||||
return num_images if type(num_images) is int and num_images > 0 else 1
|
||||
|
|
@ -165,7 +187,7 @@ def _passthrough_cost_per_image(entry: Mapping[str, object], request_body: Mappi
|
|||
|
||||
def fal_ai_passthrough_cost(model: str, request_body: Mapping[str, object]) -> float | None:
|
||||
entry: Final = _entry(f"{litellm.LlmProviders.FAL_AI.value}/{model}")
|
||||
if entry is None:
|
||||
if entry is None or "output_cost_per_billable_unit" in entry:
|
||||
return None
|
||||
cost_per_image: Final = _passthrough_cost_per_image(entry, request_body)
|
||||
if cost_per_image is None:
|
||||
|
|
@ -187,15 +209,20 @@ def cost_calculator(
|
|||
normalized_model: Final = model.removeprefix(f"{litellm.LlmProviders.FAL_AI.value}/")
|
||||
images: Final = tuple(image_response.data or ())
|
||||
deployment_prices: Final = deployment_pricing(model_info)
|
||||
deployment_billable_units_cost: Final = _billable_units_cost(deployment_prices, image_response)
|
||||
if deployment_billable_units_cost is not None:
|
||||
return deployment_billable_units_cost
|
||||
deployment_cost_per_image: Final = (
|
||||
None if deployment_prices is None else deployment_prices.get("output_cost_per_image")
|
||||
)
|
||||
if deployment_cost_per_image is not None:
|
||||
return deployment_cost_per_image * len(images)
|
||||
entry: Final = _entry(f"{litellm.LlmProviders.FAL_AI.value}/{normalized_model}")
|
||||
billable_units_cost: Final = _billable_units_cost(entry, image_response)
|
||||
if billable_units_cost is not None:
|
||||
return billable_units_cost
|
||||
params: Final[Mapping[str, object]] = optional_params or MappingProxyType({})
|
||||
resolution_cost_per_image: Final = _resolution_cost_per_image(
|
||||
_entry(f"{litellm.LlmProviders.FAL_AI.value}/{normalized_model}"), params.get("resolution")
|
||||
)
|
||||
resolution_cost_per_image: Final = _resolution_cost_per_image(entry, params.get("resolution"))
|
||||
if resolution_cost_per_image is not None:
|
||||
return resolution_cost_per_image * len(images)
|
||||
keyed_costs: Final = tuple(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams
|
|||
|
||||
from .transformation import FalAIBaseConfig
|
||||
|
||||
_BARE_APP_MARKERS: Final = ("nano-banana", "gemini-25-flash-image")
|
||||
|
||||
|
||||
class FalAINanoBananaConfig(FalAIBaseConfig):
|
||||
"""
|
||||
|
|
@ -16,6 +18,7 @@ class FalAINanoBananaConfig(FalAIBaseConfig):
|
|||
- fal-ai/gemini-25-flash-image
|
||||
- fal-ai/nano-banana-2
|
||||
- fal-ai/nano-banana-pro
|
||||
- google/nano-banana-2.1
|
||||
|
||||
Provider-specific params such as ``resolution`` ("0.5K", "1K", "2K", "4K") are
|
||||
forwarded as-is and drive the per-resolution price in the cost map.
|
||||
|
|
@ -46,7 +49,9 @@ class FalAINanoBananaConfig(FalAIBaseConfig):
|
|||
stream: bool | None = None,
|
||||
) -> str:
|
||||
base_url: Final[str] = (api_base or get_secret_str("FAL_AI_API_BASE") or self.DEFAULT_BASE_URL).rstrip("/")
|
||||
endpoint: Final = model if model.startswith("fal-ai/") else f"fal-ai/{model}"
|
||||
app: Final = model.partition("/")[0].lower()
|
||||
is_bare_app: Final = any(marker in app for marker in _BARE_APP_MARKERS)
|
||||
endpoint: Final = f"fal-ai/{model}" if is_bare_app else model
|
||||
return f"{base_url}/{endpoint}"
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import math
|
|||
import sys
|
||||
import time
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from types import MappingProxyType
|
||||
from typing import Final, TypeAlias
|
||||
|
||||
|
|
@ -75,6 +75,11 @@ _H3_MAX_PROFILE: Final[_ModelProfile] = _ModelProfile(
|
|||
reference_key="image_url",
|
||||
reference_as_list=False,
|
||||
)
|
||||
_SEEDANCE_REFERENCE_PROFILE: Final[_ModelProfile] = replace(
|
||||
_SEEDANCE_PROFILE,
|
||||
reference_key="image_urls",
|
||||
reference_as_list=True,
|
||||
)
|
||||
_QUEUE_NAMESPACES: Final[frozenset[str]] = frozenset(("workflows", "comfy"))
|
||||
_STATUS_MAP: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
|
|
@ -113,7 +118,8 @@ def _profile_for_model(model: str) -> _ModelProfile:
|
|||
return _H3_PROFILE
|
||||
if model.startswith("minimax/h3-max"):
|
||||
return _H3_MAX_PROFILE
|
||||
return _SEEDANCE_PROFILE
|
||||
is_seedance_reference: Final = model.startswith("bytedance/seedance-") and model.endswith("/reference-to-video")
|
||||
return _SEEDANCE_REFERENCE_PROFILE if is_seedance_reference else _SEEDANCE_PROFILE
|
||||
|
||||
|
||||
def _resolution_for_short_side(short_side: int, profile: _ModelProfile) -> str:
|
||||
|
|
|
|||
|
|
@ -26484,6 +26484,46 @@
|
|||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/google/nano-banana-2.1": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "fal bills this endpoint by tokens and reports the charge in the x-fal-billable-units response header, priced at output_cost_per_billable_unit dollars per unit. The per-image prices are fal's representative costs by the request's resolution field (1K default, 2K, 4K) for a short prompt, used when the header is absent"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_billable_unit": 1.0,
|
||||
"output_cost_per_image": 0.04,
|
||||
"output_cost_per_image_1K": 0.04,
|
||||
"output_cost_per_image_2K": 0.059,
|
||||
"output_cost_per_image_4K": 0.134,
|
||||
"source": "https://fal.ai/models/google/nano-banana-2.1",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/google/nano-banana-2.1/edit": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "fal bills this endpoint by tokens and reports the charge in the x-fal-billable-units response header, priced at output_cost_per_billable_unit dollars per unit. The per-image prices are fal's representative costs by the request's resolution field (1K default, 2K, 4K) for a short prompt and two 1K input images, used when the header is absent"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_billable_unit": 1.0,
|
||||
"output_cost_per_image": 0.044,
|
||||
"output_cost_per_image_1K": 0.044,
|
||||
"output_cost_per_image_2K": 0.063,
|
||||
"output_cost_per_image_4K": 0.138,
|
||||
"source": "https://fal.ai/models/google/nano-banana-2.1/edit",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/edits",
|
||||
"/v1/images/generations"
|
||||
],
|
||||
"supports_vision": true
|
||||
},
|
||||
"fal_ai/google/nano-banana-latest": {
|
||||
"alias_of": "fal_ai/google/nano-banana-2.1"
|
||||
},
|
||||
"fal_ai/google/nano-banana-latest/edit": {
|
||||
"alias_of": "fal_ai/google/nano-banana-2.1/edit"
|
||||
},
|
||||
"fal_ai/openai/gpt-image-2": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
|
|
|
|||
|
|
@ -479,6 +479,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
|
|||
output_cost_per_image_1536: ReadOnly[float | None]
|
||||
output_cost_per_image_1K: ReadOnly[float | None]
|
||||
output_cost_per_image_2K: ReadOnly[float | None]
|
||||
output_cost_per_billable_unit: ReadOnly[float | None]
|
||||
ocr_cost_per_page: float | None # for OCR models
|
||||
ocr_cost_per_page_batches: ReadOnly[float | None]
|
||||
ocr_cost_per_credit: float | None # for OCR models priced by credit
|
||||
|
|
@ -4163,6 +4164,7 @@ class CustomPricingLiteLLMParams(MirroredPricingParams):
|
|||
output_cost_per_image_1536: float | None = None
|
||||
output_cost_per_image_1K: float | None = None
|
||||
output_cost_per_image_2K: float | None = None
|
||||
output_cost_per_billable_unit: float | None = None
|
||||
input_cost_per_pixel: float | None = None
|
||||
output_cost_per_pixel: float | None = None
|
||||
|
||||
|
|
|
|||
4
litellm/ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
4
litellm/ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -37220,6 +37220,8 @@ export interface components {
|
|||
output_cost_per_audio_per_second?: number | null;
|
||||
/** Output Cost Per Audio Token */
|
||||
output_cost_per_audio_token?: number | null;
|
||||
/** Output Cost Per Billable Unit */
|
||||
output_cost_per_billable_unit?: number | null;
|
||||
/** Output Cost Per Character */
|
||||
output_cost_per_character?: number | null;
|
||||
/** Output Cost Per Character Above 128K Tokens */
|
||||
|
|
@ -53508,6 +53510,8 @@ export interface components {
|
|||
output_cost_per_audio_per_second?: number | null;
|
||||
/** Output Cost Per Audio Token */
|
||||
output_cost_per_audio_token?: number | null;
|
||||
/** Output Cost Per Billable Unit */
|
||||
output_cost_per_billable_unit?: number | null;
|
||||
/** Output Cost Per Character */
|
||||
output_cost_per_character?: number | null;
|
||||
/** Output Cost Per Character Above 128K Tokens */
|
||||
|
|
|
|||
|
|
@ -7489,6 +7489,7 @@ def get_model_info_helper(
|
|||
output_cost_per_image=_model_info.get("output_cost_per_image", None),
|
||||
output_cost_per_image_1K=_model_info.get("output_cost_per_image_1K", None),
|
||||
output_cost_per_image_2K=_model_info.get("output_cost_per_image_2K", None),
|
||||
output_cost_per_billable_unit=_model_info.get("output_cost_per_billable_unit", None),
|
||||
output_cost_per_pixel=_model_info.get("output_cost_per_pixel", 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),
|
||||
|
|
|
|||
|
|
@ -26484,6 +26484,46 @@
|
|||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/google/nano-banana-2.1": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "fal bills this endpoint by tokens and reports the charge in the x-fal-billable-units response header, priced at output_cost_per_billable_unit dollars per unit. The per-image prices are fal's representative costs by the request's resolution field (1K default, 2K, 4K) for a short prompt, used when the header is absent"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_billable_unit": 1.0,
|
||||
"output_cost_per_image": 0.04,
|
||||
"output_cost_per_image_1K": 0.04,
|
||||
"output_cost_per_image_2K": 0.059,
|
||||
"output_cost_per_image_4K": 0.134,
|
||||
"source": "https://fal.ai/models/google/nano-banana-2.1",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/google/nano-banana-2.1/edit": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "fal bills this endpoint by tokens and reports the charge in the x-fal-billable-units response header, priced at output_cost_per_billable_unit dollars per unit. The per-image prices are fal's representative costs by the request's resolution field (1K default, 2K, 4K) for a short prompt and two 1K input images, used when the header is absent"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_billable_unit": 1.0,
|
||||
"output_cost_per_image": 0.044,
|
||||
"output_cost_per_image_1K": 0.044,
|
||||
"output_cost_per_image_2K": 0.063,
|
||||
"output_cost_per_image_4K": 0.138,
|
||||
"source": "https://fal.ai/models/google/nano-banana-2.1/edit",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/edits",
|
||||
"/v1/images/generations"
|
||||
],
|
||||
"supports_vision": true
|
||||
},
|
||||
"fal_ai/google/nano-banana-latest": {
|
||||
"alias_of": "fal_ai/google/nano-banana-2.1"
|
||||
},
|
||||
"fal_ai/google/nano-banana-latest/edit": {
|
||||
"alias_of": "fal_ai/google/nano-banana-2.1/edit"
|
||||
},
|
||||
"fal_ai/openai/gpt-image-2": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ from litellm.utils import get_optional_params_image_gen
|
|||
"fal-ai/gemini-25-flash-image",
|
||||
"fal-ai/nano-banana-2",
|
||||
"fal-ai/nano-banana-pro",
|
||||
"google/nano-banana-2.1",
|
||||
"google/nano-banana-2.1/edit",
|
||||
],
|
||||
)
|
||||
def test_nano_banana_config_selected(model):
|
||||
|
|
@ -48,6 +50,9 @@ def test_imagen4_still_routes_to_imagen4_config():
|
|||
"https://fal.run/fal-ai/gemini-25-flash-image",
|
||||
),
|
||||
("nano-banana", "https://fal.run/fal-ai/nano-banana"),
|
||||
("nano-banana-2/edit", "https://fal.run/fal-ai/nano-banana-2/edit"),
|
||||
("google/nano-banana-2.1", "https://fal.run/google/nano-banana-2.1"),
|
||||
("google/nano-banana-2.1/edit", "https://fal.run/google/nano-banana-2.1/edit"),
|
||||
],
|
||||
)
|
||||
def test_get_complete_url_derives_endpoint_from_model(model, expected_url):
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ from typing import Final
|
|||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.core_helpers import set_provider_response_headers_in_hidden_params
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import CostCalculatorUtils
|
||||
from litellm.llms.fal_ai.cost_calculator import cost_calculator, fal_ai_passthrough_cost
|
||||
from litellm.llms.fal_ai.cost_calculator import FAL_BILLABLE_UNITS_HEADER, cost_calculator, fal_ai_passthrough_cost
|
||||
from litellm.types.utils import ImageObject, ImageResponse
|
||||
|
||||
|
||||
|
|
@ -98,9 +99,9 @@ def test_gpt_image_25_noncanonical_response_uses_nearest_keyed_row():
|
|||
image_response=_image_response_with_dimensions(((1536, 1024),)),
|
||||
optional_params={"quality": "low", "image_size": {"width": 1536, "height": 1024}},
|
||||
)
|
||||
expected: Final = litellm.model_cost[
|
||||
"fal_ai/low/1024-x-1536/openai/gpt-image-2.5/flare/text-to-image"
|
||||
]["output_cost_per_image"]
|
||||
expected: Final = litellm.model_cost["fal_ai/low/1024-x-1536/openai/gpt-image-2.5/flare/text-to-image"][
|
||||
"output_cost_per_image"
|
||||
]
|
||||
assert cost == expected
|
||||
|
||||
|
||||
|
|
@ -304,7 +305,9 @@ def test_passthrough_num_images_multiplies_the_per_image_rate(model):
|
|||
entry: Final = litellm.model_cost[f"fal_ai/{model}"]
|
||||
assert fal_ai_passthrough_cost(model, {"num_images": 3}) == 3 * entry["output_cost_per_image"] > 0
|
||||
assert (
|
||||
fal_ai_passthrough_cost(model, {"resolution": "4K", "num_images": 2}) == 2 * entry["output_cost_per_image_4K"] > 0
|
||||
fal_ai_passthrough_cost(model, {"resolution": "4K", "num_images": 2})
|
||||
== 2 * entry["output_cost_per_image_4K"]
|
||||
> 0
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -316,3 +319,99 @@ def test_passthrough_without_a_positive_integer_num_images_charges_one_image(num
|
|||
== litellm.model_cost["fal_ai/fal-ai/nano-banana-2"]["output_cost_per_image"]
|
||||
> 0
|
||||
)
|
||||
|
||||
|
||||
BILLABLE_UNIT_MODELS: Final = ("google/nano-banana-2.1", "google/nano-banana-2.1/edit")
|
||||
|
||||
|
||||
def _image_response_with_headers(headers: dict[str, str], num_images: int = 1) -> ImageResponse:
|
||||
response: Final = _image_response(num_images=num_images)
|
||||
set_provider_response_headers_in_hidden_params(response, headers)
|
||||
return response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", BILLABLE_UNIT_MODELS)
|
||||
def test_reported_billable_units_set_the_cost_over_the_per_image_price(model: str) -> None:
|
||||
unit_price: Final = litellm.model_cost[f"fal_ai/{model}"]["output_cost_per_billable_unit"]
|
||||
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
|
||||
model=model,
|
||||
custom_llm_provider="fal_ai",
|
||||
completion_response=_image_response_with_headers({FAL_BILLABLE_UNITS_HEADER: "0.062173746"}, num_images=2),
|
||||
optional_params={"resolution": "4K", "num_images": 2},
|
||||
)
|
||||
assert cost == pytest.approx(0.062173746 * unit_price)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("headers", [{}, {FAL_BILLABLE_UNITS_HEADER: "nan"}, {FAL_BILLABLE_UNITS_HEADER: "-1"}])
|
||||
@pytest.mark.parametrize("model", BILLABLE_UNIT_MODELS)
|
||||
def test_unusable_billable_units_fall_back_to_the_resolution_price(model: str, headers: dict[str, str]) -> None:
|
||||
entry: Final = litellm.model_cost[f"fal_ai/{model}"]
|
||||
cost: Final = cost_calculator(
|
||||
model=f"fal_ai/{model}",
|
||||
image_response=_image_response_with_headers(headers, num_images=2),
|
||||
optional_params={"resolution": "2K", "num_images": 2},
|
||||
)
|
||||
assert cost == 2 * entry["output_cost_per_image_2K"] > 0
|
||||
|
||||
|
||||
def test_billable_units_are_ignored_without_a_unit_price() -> None:
|
||||
entry: Final = litellm.model_cost["fal_ai/fal-ai/nano-banana-2"]
|
||||
assert "output_cost_per_billable_unit" not in entry
|
||||
cost: Final = cost_calculator(
|
||||
model="fal_ai/fal-ai/nano-banana-2",
|
||||
image_response=_image_response_with_headers({FAL_BILLABLE_UNITS_HEADER: "7"}),
|
||||
optional_params={},
|
||||
)
|
||||
assert cost == entry["output_cost_per_image"]
|
||||
|
||||
|
||||
def test_billable_unit_price_scales_the_reported_units(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
"fal_ai/fal-ai/megapixel-billed-model",
|
||||
{"litellm_provider": "fal_ai", "mode": "image_generation", "output_cost_per_billable_unit": 0.003},
|
||||
)
|
||||
cost: Final = cost_calculator(
|
||||
model="fal_ai/fal-ai/megapixel-billed-model",
|
||||
image_response=_image_response_with_headers({FAL_BILLABLE_UNITS_HEADER: "2"}, num_images=2),
|
||||
)
|
||||
assert cost == pytest.approx(0.006)
|
||||
|
||||
|
||||
def test_deployment_price_overrides_reported_billable_units() -> None:
|
||||
cost: Final = cost_calculator(
|
||||
model="fal_ai/google/nano-banana-2.1",
|
||||
image_response=_image_response_with_headers({FAL_BILLABLE_UNITS_HEADER: "0.5"}, num_images=2),
|
||||
model_info={"output_cost_per_image": 0.01},
|
||||
)
|
||||
assert cost == pytest.approx(0.02)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("deployment_prices", "headers", "expected"),
|
||||
[
|
||||
({"output_cost_per_billable_unit": 2.0}, {FAL_BILLABLE_UNITS_HEADER: "0.5"}, 1.0),
|
||||
(
|
||||
{"output_cost_per_billable_unit": 2.0, "output_cost_per_image": 0.01},
|
||||
{FAL_BILLABLE_UNITS_HEADER: "0.5"},
|
||||
1.0,
|
||||
),
|
||||
({"output_cost_per_billable_unit": 2.0, "output_cost_per_image": 0.01}, {}, 0.02),
|
||||
],
|
||||
)
|
||||
def test_deployment_unit_price_replaces_the_catalog_unit_price(
|
||||
deployment_prices: dict[str, float], headers: dict[str, str], expected: float
|
||||
) -> None:
|
||||
cost: Final = cost_calculator(
|
||||
model="fal_ai/google/nano-banana-2.1",
|
||||
image_response=_image_response_with_headers(headers, num_images=2),
|
||||
model_info=deployment_prices,
|
||||
)
|
||||
assert cost == pytest.approx(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", BILLABLE_UNIT_MODELS)
|
||||
@pytest.mark.parametrize("body", ({}, {"resolution": "4K", "num_images": 2}))
|
||||
def test_passthrough_does_not_price_a_submission_billed_by_reported_units(model: str, body: dict[str, object]) -> None:
|
||||
assert litellm.model_cost[f"fal_ai/{model}"]["output_cost_per_image"] > 0
|
||||
assert fal_ai_passthrough_cost(model, body) is None
|
||||
|
|
|
|||
|
|
@ -117,6 +117,19 @@ class TestFalAIVideoTransformation:
|
|||
assert "reference_image_urls" not in mapped
|
||||
assert mapped["duration"] == "5"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "expected"),
|
||||
[
|
||||
("bytedance/seedance-2.5/reference-to-video", {"image_urls": ["https://example.com/image.png"]}),
|
||||
("bytedance/seedance-2.0/fast/reference-to-video", {"image_urls": ["https://example.com/image.png"]}),
|
||||
("bytedance/seedance-2.5/image-to-video", {"image_url": "https://example.com/image.png"}),
|
||||
],
|
||||
)
|
||||
def test_map_openai_params_seedance_input_reference_follows_task(self, model: str, expected: dict[str, object]):
|
||||
mapped = self.config.map_openai_params({"input_reference": "https://example.com/image.png"}, model, False)
|
||||
|
||||
assert mapped == expected
|
||||
|
||||
def test_map_openai_params_h3_omits_auto_duration(self):
|
||||
assert self.config.map_openai_params({"seconds": "auto"}, H3_TEXT_MODEL, False) == {}
|
||||
assert self.config.map_openai_params({"seconds": "auto"}, MODEL, False) == {"duration": "auto"}
|
||||
|
|
|
|||
|
|
@ -10735,6 +10735,29 @@ class TestFalAIPassthroughRoute:
|
|||
assert "no pricing entry" in response.text
|
||||
assert not route.calls
|
||||
|
||||
def test_submit_to_endpoint_billed_by_reported_units_returns_400_without_upstream_call(
|
||||
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
"fal_ai/acme/unit-billed-model",
|
||||
{
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_billable_unit": 1.0,
|
||||
"output_cost_per_image": 0.04,
|
||||
},
|
||||
)
|
||||
with respx.mock(assert_all_called=False) as upstream:
|
||||
route = upstream.post("https://queue.fal.run/acme/unit-billed-model").mock(
|
||||
return_value=httpx.Response(200, json={"request_id": "req-1"})
|
||||
)
|
||||
response = client.post("/fal_ai/acme/unit-billed-model", json={"prompt": "a red boat"})
|
||||
|
||||
assert response.status_code == 400, response.text
|
||||
assert "no pricing entry" in response.text
|
||||
assert not route.calls
|
||||
|
||||
def test_submit_gate_prices_the_request_body_not_an_empty_one(
|
||||
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -862,6 +862,7 @@ def validate_model_cost_values(model_data, exceptions=None):
|
|||
"output_cost_per_image_1K",
|
||||
"output_cost_per_image_2K",
|
||||
"output_cost_per_image_4K",
|
||||
"output_cost_per_billable_unit",
|
||||
"input_cost_per_pixel",
|
||||
"output_cost_per_pixel",
|
||||
"cost_per_second",
|
||||
|
|
@ -1156,6 +1157,7 @@ _COST_MAP_ROW_SCHEMA: Final = {
|
|||
"output_cost_per_image_1K": {"type": "number"},
|
||||
"output_cost_per_image_2K": {"type": "number"},
|
||||
"output_cost_per_image_4K": {"type": "number"},
|
||||
"output_cost_per_billable_unit": {"type": "number"},
|
||||
"output_cost_per_image_token": {"type": "number"},
|
||||
"output_cost_per_image_token_batches": {"type": "number"},
|
||||
"output_cost_per_image_token_priority": {"type": "number"},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue