mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
feat(fal_ai): add MiniMax H3 text-to-video and reference-to-video
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9a90adad32
commit
8d73ce756a
12 changed files with 289 additions and 25 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import math
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from types import MappingProxyType
|
||||
from typing import Final, TypeAlias
|
||||
|
||||
|
|
@ -36,11 +38,33 @@ class FalAIVideoError(BaseLLMException):
|
|||
|
||||
|
||||
_ALLOWED_ASPECT_RATIOS: Final[frozenset[str]] = frozenset({"auto", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9"})
|
||||
_ALLOWED_RESOLUTIONS: Final[frozenset[str]] = frozenset({"480p", "720p", "1080p", "4k"})
|
||||
_RESOLUTION_TIERS: Final[tuple[tuple[int, str], ...]] = (
|
||||
(480, "480p"),
|
||||
(720, "720p"),
|
||||
(1080, "1080p"),
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _ModelProfile:
|
||||
resolutions: frozenset[str]
|
||||
resolution_tiers: tuple[tuple[int, str], ...]
|
||||
default_resolution: str
|
||||
integer_duration: bool
|
||||
reference_key: str
|
||||
reference_as_list: bool
|
||||
|
||||
|
||||
_SEEDANCE_PROFILE: Final[_ModelProfile] = _ModelProfile(
|
||||
resolutions=frozenset({"480p", "720p", "1080p", "4k"}),
|
||||
resolution_tiers=((480, "480p"), (720, "720p"), (1080, "1080p"), (sys.maxsize, "4k")),
|
||||
default_resolution="720p",
|
||||
integer_duration=False,
|
||||
reference_key="image_url",
|
||||
reference_as_list=False,
|
||||
)
|
||||
_H3_PROFILE: Final[_ModelProfile] = _ModelProfile(
|
||||
resolutions=frozenset({"480P", "768P", "2K", "4K"}),
|
||||
resolution_tiers=((480, "480P"), (768, "768P"), (1440, "2K"), (sys.maxsize, "4K")),
|
||||
default_resolution="2K",
|
||||
integer_duration=True,
|
||||
reference_key="reference_image_urls",
|
||||
reference_as_list=True,
|
||||
)
|
||||
_QUEUE_NAMESPACES: Final[frozenset[str]] = frozenset(("workflows", "comfy"))
|
||||
_STATUS_MAP: Final[Mapping[str, str]] = MappingProxyType(
|
||||
|
|
@ -75,8 +99,12 @@ def _duration_value(value: object) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _resolution_for_short_side(short_side: int) -> str:
|
||||
return next((resolution for threshold, resolution in _RESOLUTION_TIERS if short_side <= threshold), "4k")
|
||||
def _profile_for_model(model: str) -> _ModelProfile:
|
||||
return _H3_PROFILE if model.startswith("minimax/h3/") else _SEEDANCE_PROFILE
|
||||
|
||||
|
||||
def _resolution_for_short_side(short_side: int, profile: _ModelProfile) -> str:
|
||||
return next(resolution for threshold, resolution in profile.resolution_tiers if short_side <= threshold)
|
||||
|
||||
|
||||
def _model_path_from_request_url(raw_response: httpx.Response) -> str | None:
|
||||
|
|
@ -97,14 +125,19 @@ def _request_id_from_request_url(raw_response: httpx.Response) -> str | None:
|
|||
return segments[request_id_index] if len(segments) > request_id_index else None
|
||||
|
||||
|
||||
def _size_params(size: object) -> Mapping[str, str]:
|
||||
def _size_params(size: object, profile: _ModelProfile) -> Mapping[str, str]:
|
||||
if not isinstance(size, str):
|
||||
return MappingProxyType({})
|
||||
if size in _ALLOWED_RESOLUTIONS:
|
||||
return MappingProxyType({"resolution": size})
|
||||
if size.count("x") != 1:
|
||||
normalized_size: Final[str] = size.lower()
|
||||
canonical_resolution: Final[str | None] = next(
|
||||
(resolution for resolution in profile.resolutions if resolution.lower() == normalized_size),
|
||||
None,
|
||||
)
|
||||
if canonical_resolution is not None:
|
||||
return MappingProxyType({"resolution": canonical_resolution})
|
||||
if normalized_size.count("x") != 1:
|
||||
return MappingProxyType({})
|
||||
width_text, height_text = size.split("x")
|
||||
width_text, height_text = normalized_size.split("x")
|
||||
if not (width_text.isdigit() and height_text.isdigit()):
|
||||
return MappingProxyType({})
|
||||
width: Final[int] = int(width_text)
|
||||
|
|
@ -113,7 +146,7 @@ def _size_params(size: object) -> Mapping[str, str]:
|
|||
return MappingProxyType({})
|
||||
reduced_gcd: Final[int] = math.gcd(width, height)
|
||||
aspect_ratio: Final[str] = f"{width // reduced_gcd}:{height // reduced_gcd}"
|
||||
resolution: Final[str] = _resolution_for_short_side(min(width, height))
|
||||
resolution: Final[str] = _resolution_for_short_side(min(width, height), profile)
|
||||
if aspect_ratio in _ALLOWED_ASPECT_RATIOS:
|
||||
return MappingProxyType({"resolution": resolution, "aspect_ratio": aspect_ratio})
|
||||
return MappingProxyType({"resolution": resolution})
|
||||
|
|
@ -158,18 +191,27 @@ class FalAIVideoConfig(BaseVideoConfig):
|
|||
input_reference: Final[object] = video_create_optional_params.get("input_reference")
|
||||
if "input_reference" in video_create_optional_params and not isinstance(input_reference, str):
|
||||
raise ValueError("fal.ai needs a public image URL for input_reference")
|
||||
input_reference_params: Final[Mapping[str, str]] = (
|
||||
profile: Final[_ModelProfile] = _profile_for_model(model)
|
||||
input_reference_params: Final[Mapping[str, object]] = (
|
||||
MappingProxyType({})
|
||||
if not isinstance(input_reference, str)
|
||||
else MappingProxyType({"image_url": input_reference})
|
||||
else MappingProxyType(
|
||||
{
|
||||
profile.reference_key: (
|
||||
[input_reference] # mutable-ok: fal.ai expects a list for H3 references
|
||||
if profile.reference_as_list
|
||||
else input_reference
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
duration_params: Final[Mapping[str, str]] = (
|
||||
duration_params: Final[Mapping[str, object]] = (
|
||||
MappingProxyType({})
|
||||
if "seconds" not in video_create_optional_params
|
||||
else self._duration_params(video_create_optional_params["seconds"])
|
||||
else self._duration_params(video_create_optional_params["seconds"], profile)
|
||||
)
|
||||
size_params: Final[Mapping[str, str]] = (
|
||||
_size_params(video_create_optional_params["size"])
|
||||
_size_params(video_create_optional_params["size"], profile)
|
||||
if "size" in video_create_optional_params
|
||||
else MappingProxyType({})
|
||||
)
|
||||
|
|
@ -190,11 +232,11 @@ class FalAIVideoConfig(BaseVideoConfig):
|
|||
return mapped_params
|
||||
|
||||
@staticmethod
|
||||
def _duration_params(seconds: object) -> Mapping[str, str]:
|
||||
def _duration_params(seconds: object, profile: _ModelProfile) -> Mapping[str, object]:
|
||||
duration: Final[str | None] = _duration_value(seconds)
|
||||
if duration is None:
|
||||
raise ValueError("fal.ai seconds must be a numeric value")
|
||||
return MappingProxyType({"duration": duration})
|
||||
return MappingProxyType({"duration": int(duration) if profile.integer_duration else duration})
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
|
|
@ -251,6 +293,7 @@ class FalAIVideoConfig(BaseVideoConfig):
|
|||
request_data: Mapping[str, object] | None = None,
|
||||
) -> VideoObject:
|
||||
response_data: Final[Mapping[str, object]] = _response_data(raw_response)
|
||||
profile: Final[_ModelProfile] = _profile_for_model(model)
|
||||
request_params: Final[Mapping[str, object]] = request_data or MappingProxyType({})
|
||||
request_id: Final[str] = _response_string(response_data, "request_id")
|
||||
provider: Final[str] = custom_llm_provider or _FAL_AI_PROVIDER
|
||||
|
|
@ -262,7 +305,10 @@ class FalAIVideoConfig(BaseVideoConfig):
|
|||
key: value
|
||||
for key, value in (
|
||||
("duration_seconds", duration),
|
||||
("video_resolution", resolution if isinstance(resolution, str) else "720p"),
|
||||
(
|
||||
"video_resolution",
|
||||
resolution if isinstance(resolution, str) else profile.default_resolution,
|
||||
),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22889,6 +22889,45 @@
|
|||
"video"
|
||||
]
|
||||
},
|
||||
"fal_ai/minimax/h3/text-to-video": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "video_generation",
|
||||
"output_cost_per_second": 0.13,
|
||||
"output_cost_per_second_480p": 0.05,
|
||||
"output_cost_per_second_768p": 0.06,
|
||||
"output_cost_per_second_2k": 0.13,
|
||||
"output_cost_per_second_4k": 0.16,
|
||||
"source": "https://fal.ai/models/minimax/h3/text-to-video",
|
||||
"supported_endpoints": [
|
||||
"/v1/videos"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"fal_ai/minimax/h3/reference-to-video": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "video_generation",
|
||||
"output_cost_per_second": 0.13,
|
||||
"output_cost_per_second_480p": 0.05,
|
||||
"output_cost_per_second_768p": 0.06,
|
||||
"output_cost_per_second_2k": 0.13,
|
||||
"output_cost_per_second_4k": 0.16,
|
||||
"source": "https://fal.ai/models/minimax/h3/reference-to-video",
|
||||
"supported_endpoints": [
|
||||
"/v1/videos"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"fal_ai/bytedance/seedance-2.0/text-to-video": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "video_generation",
|
||||
|
|
|
|||
|
|
@ -539,6 +539,8 @@ class LiteLLMParamsTypedDict(TypedDict, total=False):
|
|||
output_cost_per_second: float | None
|
||||
output_cost_per_second_480p: ReadOnly[float | None]
|
||||
output_cost_per_second_720p: ReadOnly[float | None]
|
||||
output_cost_per_second_768p: ReadOnly[float | None]
|
||||
output_cost_per_second_2k: ReadOnly[float | None]
|
||||
output_cost_per_second_1080p: float | None
|
||||
output_cost_per_second_4k: ReadOnly[float | None]
|
||||
num_retries: int | None
|
||||
|
|
|
|||
|
|
@ -329,6 +329,8 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
|
|||
) # video_generation tier: key output_cost_per_second_<resolution> (e.g. 1080p, 720p)
|
||||
output_cost_per_second_480p: ReadOnly[float | None]
|
||||
output_cost_per_second_720p: ReadOnly[float | None]
|
||||
output_cost_per_second_768p: ReadOnly[float | None]
|
||||
output_cost_per_second_2k: ReadOnly[float | None]
|
||||
output_cost_per_second_4k: ReadOnly[float | None]
|
||||
ocr_cost_per_page: float | None # for OCR models
|
||||
ocr_cost_per_page_batches: ReadOnly[float | None]
|
||||
|
|
@ -3610,6 +3612,8 @@ class CustomPricingLiteLLMParams(MirroredPricingParams):
|
|||
output_cost_per_second_1080p: float | None = None
|
||||
output_cost_per_second_480p: float | None = None
|
||||
output_cost_per_second_720p: float | None = None
|
||||
output_cost_per_second_768p: float | None = None
|
||||
output_cost_per_second_2k: float | None = None
|
||||
output_cost_per_second_4k: float | None = None
|
||||
input_cost_per_pixel: float | None = None
|
||||
output_cost_per_pixel: float | None = None
|
||||
|
|
|
|||
|
|
@ -6084,6 +6084,8 @@ def _get_model_info_helper(
|
|||
output_cost_per_second_1080p=_model_info.get("output_cost_per_second_1080p", None),
|
||||
output_cost_per_second_480p=_model_info.get("output_cost_per_second_480p", None),
|
||||
output_cost_per_second_720p=_model_info.get("output_cost_per_second_720p", None),
|
||||
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_per_second=_model_info.get("output_cost_per_video_per_second", None),
|
||||
output_cost_per_image=_model_info.get("output_cost_per_image", None),
|
||||
|
|
|
|||
|
|
@ -22889,6 +22889,45 @@
|
|||
"video"
|
||||
]
|
||||
},
|
||||
"fal_ai/minimax/h3/text-to-video": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "video_generation",
|
||||
"output_cost_per_second": 0.13,
|
||||
"output_cost_per_second_480p": 0.05,
|
||||
"output_cost_per_second_768p": 0.06,
|
||||
"output_cost_per_second_2k": 0.13,
|
||||
"output_cost_per_second_4k": 0.16,
|
||||
"source": "https://fal.ai/models/minimax/h3/text-to-video",
|
||||
"supported_endpoints": [
|
||||
"/v1/videos"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"fal_ai/minimax/h3/reference-to-video": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "video_generation",
|
||||
"output_cost_per_second": 0.13,
|
||||
"output_cost_per_second_480p": 0.05,
|
||||
"output_cost_per_second_768p": 0.06,
|
||||
"output_cost_per_second_2k": 0.13,
|
||||
"output_cost_per_second_4k": 0.16,
|
||||
"source": "https://fal.ai/models/minimax/h3/reference-to-video",
|
||||
"supported_endpoints": [
|
||||
"/v1/videos"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"fal_ai/bytedance/seedance-2.0/text-to-video": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"mode": "video_generation",
|
||||
|
|
|
|||
|
|
@ -607,6 +607,10 @@
|
|||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_second_2k": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_second_480p": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
|
|
@ -619,6 +623,10 @@
|
|||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_second_768p": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_token": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@
|
|||
"tests/integration/providers/test_fal_ai_image_wire.py::test_fal_gpt_image_25_edit_inlines_upload_as_data_url_and_charges_keyed_row": [
|
||||
"other.provider_wire.fal_ai.image_edit_json_data_urls_and_keyed_pricing"
|
||||
],
|
||||
"tests/integration/providers/test_fal_ai_video_wire.py::test_fal_h3_video_create_uses_canonical_body_and_status_path": [
|
||||
"other.provider_wire.fal_ai.video_queue_create_status_and_content_download"
|
||||
],
|
||||
"tests/integration/mcp/test_mcp_lifecycle.py::test_saved_headers_reach_real_mcp_tool_and_survive_unrelated_edit": [
|
||||
"mcp.call_tool.saved_headers.reach_actual_transport"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from integration._support.client import Gateway
|
|||
from integration._support.wire import Reply, Request, wire_server
|
||||
|
||||
_MODEL: Final = "bytedance/seedance-2.5/text-to-video"
|
||||
_H3_MODEL: Final = "minimax/h3/text-to-video"
|
||||
_MP4: Final = b"\x00\x00\x00\x18ftypmp42" + uuid.uuid4().bytes * 4
|
||||
|
||||
|
||||
|
|
@ -67,3 +68,56 @@ def test_fal_video_create_status_and_content_follow_queue_wire_contract(gateway:
|
|||
("GET", f"/bytedance/seedance-2.5/requests/{request_id}"),
|
||||
("GET", f"/files/{request_id}.mp4"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.covers("other.provider_wire.fal_ai.video_queue_create_status_and_content_download")
|
||||
def test_fal_h3_video_create_uses_canonical_body_and_status_path(gateway: Gateway) -> None:
|
||||
request_id: Final = "fal-h3-req-" + uuid.uuid4().hex
|
||||
|
||||
def respond(request: Request) -> Reply:
|
||||
assert request.headers["authorization"] == "Key synthetic-fal-key"
|
||||
if request.method == "POST":
|
||||
assert request.target == f"/{_H3_MODEL}"
|
||||
assert json.loads(request.body) == {
|
||||
"prompt": "a cat playing volleyball on a beach",
|
||||
"duration": 6,
|
||||
"resolution": "2K",
|
||||
}
|
||||
return Reply(
|
||||
body=json.dumps({"status": "IN_QUEUE", "request_id": request_id, "queue_position": 0}).encode()
|
||||
)
|
||||
assert request.method == "GET"
|
||||
if request.target == f"/minimax/h3/requests/{request_id}/status":
|
||||
return Reply(body=json.dumps({"status": "COMPLETED", "request_id": request_id}).encode())
|
||||
assert request.target == f"/minimax/h3/requests/{request_id}"
|
||||
return Reply(body=json.dumps({"video": {"url": f"{wire_url}/files/{request_id}.mp4"}}).encode())
|
||||
|
||||
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
||||
wire_url: Final = wire.url
|
||||
model: Final = scenario.model(
|
||||
model=f"fal_ai/{_H3_MODEL}",
|
||||
api_base=wire.url,
|
||||
api_key="synthetic-fal-key",
|
||||
)
|
||||
created: Final = gateway.post(
|
||||
"/v1/videos",
|
||||
{
|
||||
"model": model,
|
||||
"prompt": "a cat playing volleyball on a beach",
|
||||
"seconds": 6,
|
||||
"size": "2k",
|
||||
},
|
||||
)
|
||||
assert created["status"] == "queued"
|
||||
video_id: Final = created["id"]
|
||||
status: Final = gateway.get(f"/v1/videos/{video_id}")
|
||||
assert status["status"] == "completed"
|
||||
content: Final = gateway.request("GET", f"/v1/videos/{video_id}/content")
|
||||
assert content.status_code == 200, content.text
|
||||
assert content.content == _MP4
|
||||
assert [(request.method, request.target) for request in wire.drain()] == [
|
||||
("POST", f"/{_H3_MODEL}"),
|
||||
("GET", f"/minimax/h3/requests/{request_id}/status"),
|
||||
("GET", f"/minimax/h3/requests/{request_id}"),
|
||||
("GET", f"/files/{request_id}.mp4"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ from litellm.llms.fal_ai.videos.transformation import (
|
|||
FalAIVideoError,
|
||||
_queue_request_base_path,
|
||||
)
|
||||
from litellm.llms.openai.cost_calculation import video_generation_cost
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.types.videos.utils import decode_video_id_with_provider
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
MODEL = "bytedance/seedance-2.5/text-to-video"
|
||||
H3_TEXT_MODEL = "minimax/h3/text-to-video"
|
||||
H3_REFERENCE_MODEL = "minimax/h3/reference-to-video"
|
||||
|
||||
|
||||
class TestFalAIVideoTransformation:
|
||||
|
|
@ -64,6 +67,24 @@ class TestFalAIVideoTransformation:
|
|||
with pytest.raises(ValueError, match="public image URL"):
|
||||
self.config.map_openai_params({"input_reference": b"image"}, MODEL, False)
|
||||
|
||||
def test_map_openai_params_supports_h3_profiles(self):
|
||||
url = "https://example.com/image.png"
|
||||
|
||||
assert self.config.map_openai_params({"size": "2k"}, H3_TEXT_MODEL, False) == {"resolution": "2K"}
|
||||
assert self.config.map_openai_params({"size": "1024x768"}, H3_TEXT_MODEL, False) == {
|
||||
"resolution": "768P",
|
||||
"aspect_ratio": "4:3",
|
||||
}
|
||||
mapped = self.config.map_openai_params(
|
||||
{"seconds": 6, "input_reference": url},
|
||||
H3_REFERENCE_MODEL,
|
||||
False,
|
||||
)
|
||||
assert mapped["duration"] == 6
|
||||
assert isinstance(mapped["duration"], int)
|
||||
assert mapped["reference_image_urls"] == [url]
|
||||
assert "image_url" not in mapped
|
||||
|
||||
def test_transform_video_create_request(self):
|
||||
body, files, url = self.config.transform_video_create_request(
|
||||
model=MODEL,
|
||||
|
|
@ -141,6 +162,20 @@ class TestFalAIVideoTransformation:
|
|||
assert auto_video.seconds is None
|
||||
assert auto_video.size is None
|
||||
|
||||
def test_transform_video_create_response_uses_h3_default_resolution(self):
|
||||
response = Mock(spec=httpx.Response)
|
||||
response.json.return_value = {"request_id": "abc"}
|
||||
|
||||
video = self.config.transform_video_create_response(
|
||||
model=H3_TEXT_MODEL,
|
||||
raw_response=response,
|
||||
logging_obj=self.logging_obj,
|
||||
custom_llm_provider="fal_ai",
|
||||
request_data={"duration": 5},
|
||||
)
|
||||
|
||||
assert video.usage == {"duration_seconds": 5.0, "video_resolution": "2K"}
|
||||
|
||||
def test_status_request_uses_queue_base_path(self):
|
||||
response = Mock(spec=httpx.Response)
|
||||
response.json.return_value = {"request_id": "abc"}
|
||||
|
|
@ -290,9 +325,29 @@ class TestFalAIVideoTransformation:
|
|||
}
|
||||
assert rows
|
||||
for model, row in rows.items():
|
||||
assert default_video_cost_calculator(model, 5, "fal_ai", video_resolution="480p") == (
|
||||
5 * row["output_cost_per_second_480p"]
|
||||
)
|
||||
assert default_video_cost_calculator(model, 5, "fal_ai", video_resolution="720p") == (
|
||||
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_")
|
||||
assert default_video_cost_calculator(model, 5, "fal_ai", video_resolution=tier) == 5 * value
|
||||
assert default_video_cost_calculator(model, 5, "fal_ai", video_resolution="9999p") == (
|
||||
5 * row["output_cost_per_second"]
|
||||
)
|
||||
|
||||
def test_h3_video_cost_uses_model_info_tiers(self, local_model_cost_map):
|
||||
row = litellm.model_cost[f"fal_ai/{H3_TEXT_MODEL}"]
|
||||
model_info = litellm.get_model_info(model=H3_TEXT_MODEL, custom_llm_provider="fal_ai")
|
||||
|
||||
assert video_generation_cost(
|
||||
model=H3_TEXT_MODEL,
|
||||
duration_seconds=5,
|
||||
custom_llm_provider="fal_ai",
|
||||
model_info=model_info,
|
||||
video_resolution="2K",
|
||||
) == 5 * row["output_cost_per_second_2k"]
|
||||
assert video_generation_cost(
|
||||
model=H3_TEXT_MODEL,
|
||||
duration_seconds=5,
|
||||
custom_llm_provider="fal_ai",
|
||||
model_info=model_info,
|
||||
video_resolution="768p",
|
||||
) == 5 * row["output_cost_per_second_768p"]
|
||||
|
|
|
|||
|
|
@ -619,6 +619,8 @@ def validate_model_cost_values(model_data, exceptions=None):
|
|||
"output_cost_per_second",
|
||||
"output_cost_per_second_480p",
|
||||
"output_cost_per_second_720p",
|
||||
"output_cost_per_second_768p",
|
||||
"output_cost_per_second_2k",
|
||||
"output_cost_per_second_1080p",
|
||||
"output_cost_per_second_4k",
|
||||
"input_cost_per_query",
|
||||
|
|
@ -838,6 +840,8 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
|
|||
"output_cost_per_second": {"type": "number"},
|
||||
"output_cost_per_second_480p": {"type": "number"},
|
||||
"output_cost_per_second_720p": {"type": "number"},
|
||||
"output_cost_per_second_768p": {"type": "number"},
|
||||
"output_cost_per_second_2k": {"type": "number"},
|
||||
"output_cost_per_second_1080p": {"type": "number"},
|
||||
"output_cost_per_second_4k": {"type": "number"},
|
||||
"output_cost_per_token": {"type": "number"},
|
||||
|
|
|
|||
8
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
8
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -31138,12 +31138,16 @@ export interface components {
|
|||
output_cost_per_second?: number | null;
|
||||
/** Output Cost Per Second 1080P */
|
||||
output_cost_per_second_1080p?: number | null;
|
||||
/** Output Cost Per Second 2K */
|
||||
output_cost_per_second_2k?: number | null;
|
||||
/** Output Cost Per Second 480P */
|
||||
output_cost_per_second_480p?: number | null;
|
||||
/** Output Cost Per Second 4K */
|
||||
output_cost_per_second_4k?: number | null;
|
||||
/** Output Cost Per Second 720P */
|
||||
output_cost_per_second_720p?: number | null;
|
||||
/** Output Cost Per Second 768P */
|
||||
output_cost_per_second_768p?: number | null;
|
||||
/** Output Cost Per Token */
|
||||
output_cost_per_token?: number | null;
|
||||
/** Output Cost Per Token Above 128K Tokens */
|
||||
|
|
@ -41878,12 +41882,16 @@ export interface components {
|
|||
output_cost_per_second?: number | null;
|
||||
/** Output Cost Per Second 1080P */
|
||||
output_cost_per_second_1080p?: number | null;
|
||||
/** Output Cost Per Second 2K */
|
||||
output_cost_per_second_2k?: number | null;
|
||||
/** Output Cost Per Second 480P */
|
||||
output_cost_per_second_480p?: number | null;
|
||||
/** Output Cost Per Second 4K */
|
||||
output_cost_per_second_4k?: number | null;
|
||||
/** Output Cost Per Second 720P */
|
||||
output_cost_per_second_720p?: number | null;
|
||||
/** Output Cost Per Second 768P */
|
||||
output_cost_per_second_768p?: number | null;
|
||||
/** Output Cost Per Token */
|
||||
output_cost_per_token?: number | null;
|
||||
/** Output Cost Per Token Above 128K Tokens */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue