feat(fal_ai): bill flat per-video fal.ai models with output_cost_per_video (#51225)

* fix(fal_ai): support flat per-video output pricing

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fal_ai): handle flat-priced video catalog rows

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(rust): format flat video pricing parser test

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kenneth-berri 2026-11-12 15:13:49 -08:00 • committed by GitHub
parent b251f125a7
commit f298400060
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1009 additions and 19 deletions

View file

@ -2070,25 +2070,21 @@ def completion_cost(
litellm_logging_obj, custom_pricing, router_model_id
)
usage_obj = getattr(completion_response, "usage", None)
_video_usage_obj: object = attribute_or_none(completion_response, "usage")
duration_seconds: float | None = None
video_resolution: str | None = None
provider_reported_cost: float | None = None
if completion_response is not None and usage_obj:
# Handle both dict and Pydantic Usage object
if isinstance(usage_obj, dict):
duration_seconds = usage_obj.get("duration_seconds", None)
_vr = usage_obj.get("video_resolution", None)
provider_reported_cost = usage_obj.get("provider_reported_cost_usd", None)
_vc = usage_obj.get("video_count", None)
else:
duration_seconds = getattr(usage_obj, "duration_seconds", None)
_vr = getattr(usage_obj, "video_resolution", None)
provider_reported_cost = getattr(usage_obj, "provider_reported_cost_usd", None)
_vc = getattr(usage_obj, "video_count", None)
if _vr is not None:
video_resolution = str(_vr).strip().lower()
video_count = _vc if isinstance(_vc, int) and not isinstance(_vc, bool) and _vc > 1 else 1
if completion_response is not None and _video_usage_obj:
duration_seconds = _VIDEO_USAGE_NUMBER.validate_python(
_video_usage_value(_video_usage_obj, "duration_seconds")
)
provider_reported_cost = _VIDEO_USAGE_NUMBER.validate_python(
_video_usage_value(_video_usage_obj, "provider_reported_cost_usd")
)
if (
video_resolution_value := _video_usage_value(_video_usage_obj, "video_resolution")
) is not None:
video_resolution = str(video_resolution_value).strip().lower()
if _video_model_info is None and provider_reported_cost is not None:
return float(provider_reported_cost)
@ -2105,7 +2101,7 @@ def completion_cost(
custom_llm_provider=custom_llm_provider,
model_info=_video_model_info,
video_resolution=video_resolution,
) * video_count + _video_input_image_cost(
) * _video_count(_video_usage_obj) + _video_input_image_cost(
requested_model or model,
custom_llm_provider,
_video_model_info,
@ -2118,7 +2114,7 @@ def completion_cost(
custom_llm_provider=custom_llm_provider,
model_info=_video_model_info,
video_resolution=video_resolution,
)
) * _video_count(_video_usage_obj)
elif call_type in _SPEECH_CALL_TYPES:
prompt_characters = _speech_billed_characters(
completion_response if isinstance(completion_response, HttpxBinaryResponseContent) else None,
@ -2631,6 +2627,19 @@ def _speech_billed_characters(speech_response: HttpxBinaryResponseContent | None
return reported if isinstance(reported, int) else litellm.utils.count_characters(text=prompt)
_VIDEO_USAGE_NUMBER: Final[TypeAdapter[float | None]] = TypeAdapter(float | None)
def _video_usage_value(usage: object, field: str) -> object:
fields: Final = as_str_mapping(usage)
return fields.get(field) if fields is not None else attribute_or_none(usage, field)
def _video_count(usage: object) -> int:
count: Final = _video_usage_value(usage, "video_count")
return count if type(count) is int and count > 1 else 1
def _video_input_image_cost(
model: str,
custom_llm_provider: str | None,
@ -3184,6 +3193,10 @@ def default_video_cost_calculator(
if cost_info is None:
raise Exception(f"Model not found in cost map for model={model}")
video_cost: Final = cost_info.get("output_cost_per_video")
if video_cost is not None:
return video_cost
# Check for video-specific cost per second first
video_cost_per_second: Final = cost_info.get("output_cost_per_video_per_second")
if video_cost_per_second is not None:

View file

@ -189,6 +189,9 @@ def fal_ai_passthrough_cost(model: str, request_body: Mapping[str, object]) -> f
entry: Final = _entry(f"{litellm.LlmProviders.FAL_AI.value}/{model}")
if entry is None or "output_cost_per_billable_unit" in entry:
return None
cost_per_video: Final = entry.get("output_cost_per_video")
if isinstance(cost_per_video, (int, float)):
return float(cost_per_video)
cost_per_image: Final = _passthrough_cost_per_image(entry, request_body)
if cost_per_image is None:
return None

View file

@ -162,6 +162,10 @@ def video_generation_cost(
if model_info is None:
model_info = get_model_info(model=model, custom_llm_provider=custom_llm_provider or "openai")
video_cost: Final = model_info.get("output_cost_per_video")
if video_cost is not None:
return video_cost
# Check for video-specific cost per second
video_cost_per_second: Final = model_info.get("output_cost_per_video_per_second")
if video_cost_per_second is not None:

View file

@ -95168,6 +95168,438 @@
"video"
]
},
"fal_ai/fal-ai/auto-caption": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.1,
"source": "https://fal.ai/models/fal-ai/auto-caption",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/hunyuan-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.4,
"source": "https://fal.ai/models/fal-ai/hunyuan-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/hunyuan-video-image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.4,
"source": "https://fal.ai/models/fal-ai/hunyuan-video-image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/infinity-star/text-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.07,
"source": "https://fal.ai/models/fal-ai/infinity-star/text-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.02,
"source": "https://fal.ai/models/fal-ai/ltx-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video-13b-distilled": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.04,
"source": "https://fal.ai/models/fal-ai/ltx-video-13b-distilled",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video-13b-distilled/extend": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.04,
"source": "https://fal.ai/models/fal-ai/ltx-video-13b-distilled/extend",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video-13b-distilled/multiconditioning": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.04,
"source": "https://fal.ai/models/fal-ai/ltx-video-13b-distilled/multiconditioning",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.02,
"source": "https://fal.ai/models/fal-ai/ltx-video/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/hailuo-2.3-fast/pro/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.33,
"source": "https://fal.ai/models/fal-ai/minimax/hailuo-2.3-fast/pro/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-director": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-director",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-director/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-director/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-live": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-live",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-live/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-live/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-subject-reference": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-subject-reference",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ovi": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.2,
"source": "https://fal.ai/models/fal-ai/ovi",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ovi/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.2,
"source": "https://fal.ai/models/fal-ai/ovi/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-i2v-lora": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.75,
"source": "https://fal.ai/models/fal-ai/wan-i2v-lora",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-pro/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.8,
"source": "https://fal.ai/models/fal-ai/wan-pro/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-t2v-lora": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.75,
"source": "https://fal.ai/models/fal-ai/wan-t2v-lora",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan/v2.2-5b/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.15,
"source": "https://fal.ai/models/fal-ai/wan/v2.2-5b/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan/v2.2-5b/text-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.15,
"source": "https://fal.ai/models/fal-ai/wan/v2.2-5b/text-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/moonvalley/marey/motion-transfer": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 2,
"source": "https://fal.ai/models/moonvalley/marey/motion-transfer",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/moonvalley/marey/pose-transfer": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 2,
"source": "https://fal.ai/models/moonvalley/marey/pose-transfer",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/mureka/api/generate/lyrics-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.1,
"source": "https://fal.ai/models/mureka/api/generate/lyrics-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-22-vace-fun-a14b/inpainting": {
"litellm_provider": "fal_ai",
"metadata": {"comment":"per second of generated video by resolution; the page's resolution default is auto or unstated, so output_cost_per_second is the highest tier (720p); tiers without a catalog field: 580p $0.075"},

View file

@ -120,6 +120,7 @@ pub struct ModelInfoFields {
pub output_cost_per_second_768p: Option<f64>,
pub output_cost_per_second_2k: Option<f64>,
pub output_cost_per_second_4k: Option<f64>,
pub output_cost_per_video: Option<f64>,
pub output_cost_per_video_per_second: Option<f64>,
pub output_cost_per_image: Option<f64>,
pub output_cost_per_billable_unit: Option<f64>,

View file

@ -869,6 +869,7 @@ class LiteLLMParamsTypedDict(TypedDict, total=False):
cost_per_second: ReadOnly[float | None]
input_cost_per_second: float | None
input_cost_per_text_message: ReadOnly[float | None]
output_cost_per_video: ReadOnly[float | None]
output_cost_per_second: float | None
output_cost_per_second_480p: ReadOnly[float | None]
output_cost_per_second_720p: ReadOnly[float | None]

View file

@ -463,6 +463,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
output_cost_per_reasoning_token: float | None
output_cost_per_reasoning_token_flex: float | None
output_cost_per_reasoning_token_priority: float | None
output_cost_per_video: ReadOnly[float | None]
output_cost_per_video_per_second: float | None # only for vertex ai models
output_cost_per_audio_per_second: float | None # only for vertex ai models
output_cost_per_second: float | None # for OpenAI Speech models
@ -4268,6 +4269,7 @@ class CustomPricingLiteLLMParams(MirroredPricingParams):
output_cost_per_reasoning_token: float | None = None
output_cost_per_reasoning_token_flex: float | None = None
output_cost_per_reasoning_token_priority: float | None = None
output_cost_per_video: float | None = None
output_cost_per_video_per_second: float | None = None
output_cost_per_audio_per_second: float | None = None
search_context_cost_per_query: dict[str, Any] | None = None

View file

@ -37621,6 +37621,8 @@ export interface components {
output_cost_per_token_priority?: number | null;
/** Output Cost Per Token Ultrafast */
output_cost_per_token_ultrafast?: number | null;
/** Output Cost Per Video */
output_cost_per_video?: number | null;
/** Output Cost Per Video Per Second */
output_cost_per_video_per_second?: number | null;
/** Output Cost Per Video Token */
@ -54107,6 +54109,8 @@ export interface components {
output_cost_per_token_priority?: number | null;
/** Output Cost Per Token Ultrafast */
output_cost_per_token_ultrafast?: number | null;
/** Output Cost Per Video */
output_cost_per_video?: number | null;
/** Output Cost Per Video Per Second */
output_cost_per_video_per_second?: number | null;
/** Output Cost Per Video Token */

View file

@ -7485,6 +7485,7 @@ def get_model_info_helper(
output_cost_per_second_768p=_model_info.get("output_cost_per_second_768p", None),
output_cost_per_second_2k=_model_info.get("output_cost_per_second_2k", None),
output_cost_per_second_4k=_model_info.get("output_cost_per_second_4k", None),
output_cost_per_video=_model_info.get("output_cost_per_video", None),
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_image_1K=_model_info.get("output_cost_per_image_1K", None),
@ -7667,6 +7668,7 @@ def get_model_info(
] # only for vertex ai models
output_cost_per_image: Optional[float]
output_vector_size: Optional[int]
output_cost_per_video: Optional[float]
output_cost_per_video_per_second: Optional[float] # only for vertex ai models
output_cost_per_audio_per_second: Optional[float] # only for vertex ai models
litellm_provider: Required[str]

View file

@ -95168,6 +95168,438 @@
"video"
]
},
"fal_ai/fal-ai/auto-caption": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.1,
"source": "https://fal.ai/models/fal-ai/auto-caption",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/hunyuan-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.4,
"source": "https://fal.ai/models/fal-ai/hunyuan-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/hunyuan-video-image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.4,
"source": "https://fal.ai/models/fal-ai/hunyuan-video-image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/infinity-star/text-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.07,
"source": "https://fal.ai/models/fal-ai/infinity-star/text-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.02,
"source": "https://fal.ai/models/fal-ai/ltx-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video-13b-distilled": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.04,
"source": "https://fal.ai/models/fal-ai/ltx-video-13b-distilled",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video-13b-distilled/extend": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.04,
"source": "https://fal.ai/models/fal-ai/ltx-video-13b-distilled/extend",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video-13b-distilled/multiconditioning": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.04,
"source": "https://fal.ai/models/fal-ai/ltx-video-13b-distilled/multiconditioning",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ltx-video/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.02,
"source": "https://fal.ai/models/fal-ai/ltx-video/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/hailuo-2.3-fast/pro/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.33,
"source": "https://fal.ai/models/fal-ai/minimax/hailuo-2.3-fast/pro/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-director": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-director",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-director/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-director/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-live": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-live",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-live/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-live/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01-subject-reference": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01-subject-reference",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/minimax/video-01/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.5,
"source": "https://fal.ai/models/fal-ai/minimax/video-01/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ovi": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.2,
"source": "https://fal.ai/models/fal-ai/ovi",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/ovi/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.2,
"source": "https://fal.ai/models/fal-ai/ovi/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-i2v-lora": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.75,
"source": "https://fal.ai/models/fal-ai/wan-i2v-lora",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-pro/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.8,
"source": "https://fal.ai/models/fal-ai/wan-pro/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-t2v-lora": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.75,
"source": "https://fal.ai/models/fal-ai/wan-t2v-lora",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan/v2.2-5b/image-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.15,
"source": "https://fal.ai/models/fal-ai/wan/v2.2-5b/image-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan/v2.2-5b/text-to-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.15,
"source": "https://fal.ai/models/fal-ai/wan/v2.2-5b/text-to-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/moonvalley/marey/motion-transfer": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 2,
"source": "https://fal.ai/models/moonvalley/marey/motion-transfer",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/moonvalley/marey/pose-transfer": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 2,
"source": "https://fal.ai/models/moonvalley/marey/pose-transfer",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/mureka/api/generate/lyrics-video": {
"litellm_provider": "fal_ai",
"mode": "video_generation",
"output_cost_per_video": 0.1,
"source": "https://fal.ai/models/mureka/api/generate/lyrics-video",
"supported_endpoints": [
"/v1/videos"
],
"supported_modalities": [
"text",
"video"
],
"supported_output_modalities": [
"video"
]
},
"fal_ai/fal-ai/wan-22-vace-fun-a14b/inpainting": {
"litellm_provider": "fal_ai",
"metadata": {"comment":"per second of generated video by resolution; the page's resolution default is auto or unstated, so output_cost_per_second is the highest tier (720p); tiers without a catalog field: 580p $0.075"},

View file

@ -3,9 +3,11 @@ from typing import Final
import pytest
import litellm
from litellm.cost_calculator import default_video_cost_calculator
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 FAL_BILLABLE_UNITS_HEADER, cost_calculator, fal_ai_passthrough_cost
from litellm.llms.openai.cost_calculation import video_generation_cost
from litellm.types.utils import ImageObject, ImageResponse
@ -34,6 +36,20 @@ def _image_response_with_dimensions(dimensions: tuple[tuple[int, int], ...]) ->
)
@pytest.mark.parametrize("duration", [0.0, 8.0])
def test_catalog_flat_video_price_reaches_both_video_calculators(
monkeypatch: pytest.MonkeyPatch, duration: float
) -> None:
monkeypatch.setitem(
litellm.model_cost,
"fal_ai/fal-ai/test-flat-video",
{"litellm_provider": "fal_ai", "mode": "video_generation", "output_cost_per_video": 0.37},
)
assert video_generation_cost("fal-ai/test-flat-video", duration, "fal_ai") == pytest.approx(0.37)
assert default_video_cost_calculator("fal-ai/test-flat-video", duration, "fal_ai") == pytest.approx(0.37)
assert fal_ai_passthrough_cost("fal-ai/test-flat-video", {}) == pytest.approx(0.37)
GPT_IMAGE_25_MODELS = (
"openai/gpt-image-2.5/flare/text-to-image",
"openai/gpt-image-2.5/flare/edit",

View file

@ -722,7 +722,8 @@ class TestFalAIVideoTransformation:
assert isinstance(provider_config, FalAIVideoConfig)
assert isinstance(self.config.get_error_class("bad key", 401, {}), FalAIVideoError)
def test_video_cost_uses_tiered_rows(self):
@pytest.mark.parametrize(("duration", "resolution"), [(0, None), (5, "720p"), (17, "9999p")])
def test_video_cost_uses_tiered_rows(self, duration: float, resolution: str | None) -> None:
rows = {
model: row
for model, row in litellm.model_cost.items()
@ -730,6 +731,9 @@ class TestFalAIVideoTransformation:
}
assert rows
for model, row in rows.items():
if (flat_cost := row.get("output_cost_per_video")) is not None:
assert default_video_cost_calculator(model, duration, "fal_ai", video_resolution=resolution) == flat_cost
continue
for key, value in row.items():
if key.startswith("output_cost_per_second_") and value is not None:
tier = key.removeprefix("output_cost_per_second_")

View file

@ -87,6 +87,22 @@ def test_handler_charges_nothing_and_names_the_model_for_queue_status_and_result
assert logging_obj.model_call_details["response_cost"] is None
def test_handler_charges_flat_video_queue_submit(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setitem(
litellm.model_cost,
"fal_ai/fal-ai/test-flat-video",
{"litellm_provider": "fal_ai", "mode": "video_generation", "output_cost_per_video": 0.37},
)
result: Final = FalAIPassthroughLoggingHandler().fal_ai_passthrough_handler(
response_body={"request_id": "req-1", "status": "IN_QUEUE"},
request_body={},
logging_obj=_logging_obj(),
url_route="https://queue.fal.run/fal-ai/test-flat-video",
kwargs={},
)
assert result["kwargs"]["response_cost"] == pytest.approx(0.37)
def test_handler_charges_for_queue_submit():
handler_result = FalAIPassthroughLoggingHandler().fal_ai_passthrough_handler(
response_body={"request_id": "req-1", "status": "IN_QUEUE"},

View file

@ -10663,6 +10663,20 @@ class TestFalAIPassthroughRoute:
assert sent.headers["authorization"] == "Key fal-test-key"
assert json.loads(sent.content or b"{}") == body
def test_flat_video_submit_is_accepted(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setitem(
litellm.model_cost,
"fal_ai/fal-ai/test-flat-video",
{"litellm_provider": "fal_ai", "mode": "video_generation", "output_cost_per_video": 0.37},
)
with respx.mock(assert_all_called=True) as upstream:
upstream.post("https://queue.fal.run/fal-ai/test-flat-video").mock(
return_value=httpx.Response(200, json={"request_id": "req-flat", "status": "IN_QUEUE"})
)
response: Final = client.post("/fal_ai/fal-ai/test-flat-video", json={"prompt": "A lake"})
assert response.status_code == 200, response.text
assert response.json() == {"request_id": "req-flat", "status": "IN_QUEUE"}
def test_status_get_forwards_to_queue_fal_run(self, client: TestClient) -> None:
with respx.mock(assert_all_called=True) as upstream:
route = upstream.get("https://queue.fal.run/fal-ai/trellis-2/requests/req-1/status").mock(

View file

@ -1183,6 +1183,7 @@ _COST_MAP_ROW_SCHEMA: Final = {
"output_cost_per_token_above_512k_tokens": {"type": "number"},
"output_cost_per_token_batches": {"type": "number"},
"output_cost_per_reasoning_token": {"type": "number"},
"output_cost_per_video": {"type": "number"},
"output_cost_per_video_per_second": {"type": "number"},
"output_db_cost_per_token": {"type": "number"},
"output_dbu_cost_per_token": {"type": "number"},

View file

@ -2,6 +2,7 @@ import asyncio
import io
import json
import os
from datetime import datetime
from typing import Final
from unittest.mock import AsyncMock, MagicMock, patch
@ -19,6 +20,8 @@ from litellm.llms.gemini.videos.transformation import GeminiVideoConfig
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.videos.main import VideoObject, VideoResponse
from litellm.types.utils import ModelInfo
from litellm.llms.openai.cost_calculation import video_generation_cost
from litellm.videos import main as videos_main
from litellm.videos.main import (
avideo_generation,
@ -29,6 +32,48 @@ from litellm.videos.main import (
from tests.unit.proxy.router_entry_point_stubs import route_entry_points_to_stubs
@pytest.mark.parametrize("duration", [0.0, 5.0, 30.0])
@pytest.mark.parametrize("price", [0.0, 0.37])
def test_flat_video_price_takes_precedence_over_duration(duration: float, price: float) -> None:
pricing: Final[ModelInfo] = {"output_cost_per_video": price, "output_cost_per_video_per_second": 0.12}
assert video_generation_cost("test-flat-video", duration, model_info=pricing) == price
assert default_video_cost_calculator("test-flat-video", duration, model_info=pricing) == price
@pytest.mark.parametrize("duration", [None, 0.0, 12.0])
@pytest.mark.parametrize("count", [1, 3])
def test_completion_cost_flat_video_deployment_override_counts_videos(
monkeypatch: pytest.MonkeyPatch, duration: float | None, count: int
) -> None:
monkeypatch.setitem(
litellm.model_cost,
"openai/test-flat-video",
{"litellm_provider": "openai", "mode": "video_generation", "output_cost_per_video": 0.91},
)
response: Final = VideoObject(
id="vid-flat", object="video", status="completed", usage={"duration_seconds": duration, "video_count": count}
)
logging_obj: Final = LitellmLogging(
model="openai/test-flat-video",
messages=[],
stream=False,
call_type="create_video",
start_time=datetime(2026, 1, 1),
litellm_call_id="flat-video",
function_id="flat-video",
)
logging_obj.litellm_params = {"metadata": {"model_info": {"output_cost_per_video": 0.37}}}
cost: Final = litellm.completion_cost(
completion_response=response,
model="openai/test-flat-video",
call_type="create_video",
custom_llm_provider="openai",
custom_pricing=True,
litellm_logging_obj=logging_obj,
)
assert cost == pytest.approx(0.37 * count)
class TestVideoGeneration:
"""Test suite for video generation functionality."""