mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
Merge pull request #24899 from Sameerlite/litellm_gemini-veo-video-resolution-pricing
feat(gemini): Veo Lite pricing, video resolution usage and tiered cost
This commit is contained in:
commit
1acdf912fb
12 changed files with 804 additions and 417 deletions
|
|
@ -9,8 +9,8 @@ LiteLLM supports Google's Veo video generation models through a unified API inte
|
|||
|-------|-------|
|
||||
| Description | Google's Veo AI video generation models |
|
||||
| Provider Route on LiteLLM | `gemini/` |
|
||||
| Supported Models | `veo-3.0-generate-preview`, `veo-3.1-generate-preview` |
|
||||
| Cost Tracking | ✅ Duration-based pricing |
|
||||
| Supported Models | Veo 3.0 / 3.1 preview and production IDs (see table below), including **Veo 3.1 Lite** |
|
||||
| Cost Tracking | ✅ Duration-based pricing; optional **per-resolution** tiers where the catalog lists them (e.g. 720p vs 1080p) |
|
||||
| Logging Support | ✅ Full request/response logging |
|
||||
| Proxy Server Support | ✅ Full proxy integration with virtual keys |
|
||||
| Spend Management | ✅ Budget tracking and rate limiting |
|
||||
|
|
@ -79,6 +79,11 @@ print("Video downloaded successfully!")
|
|||
|------------|-------------|--------------|--------|
|
||||
| veo-3.0-generate-preview | Veo 3.0 video generation | 8 seconds | Preview |
|
||||
| veo-3.1-generate-preview | Veo 3.1 video generation | 8 seconds | Preview |
|
||||
| veo-3.1-lite-generate-preview | Veo 3.1 **Lite** (cost-efficient; [Gemini pricing](https://ai.google.dev/gemini-api/docs/video)) | Per Google docs | Preview |
|
||||
| veo-3.1-fast-generate-preview / `…-001` | Faster / prod variants | Per Google docs | Preview / GA |
|
||||
| veo-3.1-generate-001 | Veo 3.1 production | Per Google docs | GA |
|
||||
|
||||
Use the full LiteLLM model id with the `gemini/` prefix (for example `gemini/veo-3.1-lite-generate-preview`).
|
||||
|
||||
## Video Generation Parameters
|
||||
|
||||
|
|
@ -87,14 +92,29 @@ LiteLLM automatically maps OpenAI-style parameters to Veo's format:
|
|||
| OpenAI Parameter | Veo Parameter | Description | Example |
|
||||
|------------------|---------------|-------------|---------|
|
||||
| `prompt` | `prompt` | Text description of the video | "A cat playing" |
|
||||
| `size` | `aspectRatio` | Video dimensions → aspect ratio | "1280x720" → "16:9" |
|
||||
| `size` | `aspectRatio` and, when applicable, **`resolution`** | Standard widths/heights map to landscape/portrait **and** to `720p` or `1080p` for the API | See below |
|
||||
| `seconds` | `durationSeconds` | Duration in seconds | "8" → 8 |
|
||||
| `input_reference` | `image` | Reference image to animate | File object or path |
|
||||
| `model` | `model` | Model to use | "gemini/veo-3.0-generate-preview" |
|
||||
|
||||
### Size to Aspect Ratio Mapping
|
||||
### `size` and output resolution
|
||||
|
||||
When you pass a **standard `size`** string, LiteLLM sets both:
|
||||
|
||||
- **Aspect ratio** (`16:9` or `9:16`) — same as before.
|
||||
- **Output resolution** (`720p` or `1080p`) when the height is clear from the preset, so the correct Veo tier is requested without extra fields.
|
||||
|
||||
| `size` | Aspect ratio | Resolution sent to Veo |
|
||||
|--------|----------------|-------------------------|
|
||||
| `1280x720`, `720x1280` | `16:9` / `9:16` | `720p` |
|
||||
| `1920x1080`, `1080x1920` | `16:9` / `9:16` | `1080p` |
|
||||
|
||||
Other `size` values still map to an aspect ratio (defaulting to `16:9` when unknown); resolution is left to **Google’s default** unless you set it yourself.
|
||||
|
||||
You can also pass Veo’s **`resolution`** (for example via `extra_body`) if you need an explicit value that does not match the presets above. If you set `resolution` yourself, it takes precedence over the value inferred from `size`.
|
||||
|
||||
### Size to aspect ratio (reference)
|
||||
|
||||
LiteLLM automatically converts size dimensions to Veo's aspect ratio format:
|
||||
- `"1280x720"`, `"1920x1080"` → `"16:9"` (landscape)
|
||||
- `"720x1280"`, `"1080x1920"` → `"9:16"` (portrait)
|
||||
|
||||
|
|
@ -293,7 +313,14 @@ with open("video.mp4", "wb") as f:
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Cost Tracking
|
||||
## Cost tracking and spend
|
||||
|
||||
LiteLLM estimates **video spend** from:
|
||||
|
||||
1. **How long** the generated clip is billed for (seconds), and
|
||||
2. **The per-second price** for that model in LiteLLM’s model catalog (aligned with [Google’s Gemini API video pricing](https://ai.google.dev/gemini-api/docs/video) where applicable).
|
||||
|
||||
Some models charge **different per-second rates** for **720p** vs **1080p**. When you use the standard `size` presets above (or set `resolution` explicitly), LiteLLM uses the matching tier so **proxy spend, logs, and budgets** line up with the resolution you requested.
|
||||
|
||||
LiteLLM automatically tracks costs for Veo video generation:
|
||||
|
||||
|
|
@ -314,8 +341,8 @@ response = litellm.video_generation(
|
|||
| Feature | OpenAI (Sora) | Gemini (Veo) |
|
||||
|---------|---------------|--------------|
|
||||
| Reference Images | ✅ Supported | ❌ Not supported |
|
||||
| Size Control | ✅ Supported | ❌ Not supported |
|
||||
| Duration Control | ✅ Supported | ❌ Not supported |
|
||||
| Size / dimensions | ✅ Supported | ✅ Supported via `size` → aspect ratio + `720p`/`1080p` where preset |
|
||||
| Duration (`seconds`) | ✅ Supported | ✅ Supported (maps to `durationSeconds`; limits per Google docs) |
|
||||
| Video Remix/Edit | ✅ Supported | ❌ Not supported |
|
||||
| Video List | ✅ Supported | ❌ Not supported |
|
||||
| Prompt-based Generation | ✅ Supported | ✅ Supported |
|
||||
|
|
|
|||
|
|
@ -1144,15 +1144,16 @@ def completion_cost( # noqa: PLR0915
|
|||
if isinstance(usage_obj, BaseModel) and not _is_known_usage_objects(
|
||||
usage_obj=usage_obj
|
||||
):
|
||||
_usage_for_dump = cast(BaseModel, usage_obj)
|
||||
setattr(
|
||||
completion_response,
|
||||
"usage",
|
||||
litellm.Usage(**usage_obj.model_dump()),
|
||||
litellm.Usage(**_usage_for_dump.model_dump()),
|
||||
)
|
||||
if usage_obj is None:
|
||||
_usage = {}
|
||||
elif isinstance(usage_obj, BaseModel):
|
||||
_usage = usage_obj.model_dump()
|
||||
_usage = cast(BaseModel, usage_obj).model_dump()
|
||||
else:
|
||||
_usage = usage_obj
|
||||
|
||||
|
|
@ -1279,14 +1280,20 @@ def completion_cost( # noqa: PLR0915
|
|||
_video_model_info = _metadata.get("model_info", None)
|
||||
|
||||
usage_obj = getattr(completion_response, "usage", None)
|
||||
duration_seconds: Optional[float] = None
|
||||
video_resolution: Optional[str] = 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)
|
||||
else:
|
||||
duration_seconds = getattr(
|
||||
usage_obj, "duration_seconds", None
|
||||
)
|
||||
_vr = getattr(usage_obj, "video_resolution", None)
|
||||
if _vr is not None:
|
||||
video_resolution = str(_vr).strip().lower()
|
||||
|
||||
if duration_seconds is not None:
|
||||
# Calculate cost based on video duration using video-specific cost calculation
|
||||
|
|
@ -1299,6 +1306,7 @@ def completion_cost( # noqa: PLR0915
|
|||
duration_seconds=duration_seconds,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model_info=_video_model_info,
|
||||
video_resolution=video_resolution,
|
||||
)
|
||||
# Fallback to default video cost calculation if no duration available
|
||||
return default_video_cost_calculator(
|
||||
|
|
@ -1306,6 +1314,7 @@ def completion_cost( # noqa: PLR0915
|
|||
duration_seconds=0.0, # Default to 0 if no duration available
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model_info=_video_model_info,
|
||||
video_resolution=video_resolution,
|
||||
)
|
||||
elif call_type in _SPEECH_CALL_TYPES:
|
||||
prompt_characters = litellm.utils._count_characters(text=prompt)
|
||||
|
|
@ -1626,7 +1635,7 @@ def get_response_cost_from_hidden_params(
|
|||
hidden_params: Union[dict, BaseModel],
|
||||
) -> Optional[float]:
|
||||
if isinstance(hidden_params, BaseModel):
|
||||
_hidden_params_dict = hidden_params.model_dump()
|
||||
_hidden_params_dict = cast(BaseModel, hidden_params).model_dump()
|
||||
else:
|
||||
_hidden_params_dict = hidden_params
|
||||
|
||||
|
|
@ -1963,6 +1972,7 @@ def default_video_cost_calculator(
|
|||
duration_seconds: float,
|
||||
custom_llm_provider: Optional[str] = None,
|
||||
model_info: Optional[ModelInfo] = None,
|
||||
video_resolution: Optional[str] = None,
|
||||
) -> float:
|
||||
"""
|
||||
Default video cost calculator for video generation
|
||||
|
|
@ -1974,6 +1984,7 @@ def default_video_cost_calculator(
|
|||
model_info (Optional[ModelInfo]): Deployment-level model info containing
|
||||
custom video pricing. When provided, used before falling back to
|
||||
the global litellm.model_cost lookup.
|
||||
video_resolution (Optional[str]): From usage (e.g. ``720p``, ``1080p``) for tiered per-second pricing.
|
||||
|
||||
Returns:
|
||||
float: Cost in USD for the video generation
|
||||
|
|
@ -2027,8 +2038,9 @@ def default_video_cost_calculator(
|
|||
if video_cost_per_second is not None:
|
||||
return video_cost_per_second * duration_seconds
|
||||
|
||||
# Fallback to general output cost per second
|
||||
output_cost_per_second = cost_info.get("output_cost_per_second")
|
||||
from litellm.llms.openai.cost_calculation import _video_output_cost_per_second
|
||||
|
||||
output_cost_per_second = _video_output_cost_per_second(cost_info, video_resolution)
|
||||
if output_cost_per_second is not None:
|
||||
return output_cost_per_second * duration_seconds
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ def _convert_image_to_gemini_format(image_file) -> Dict[str, str]:
|
|||
return {"bytesBase64Encoded": base64_encoded, "mimeType": mime_type}
|
||||
|
||||
|
||||
def _usage_video_resolution_from_parameters(
|
||||
parameters: Dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""Normalize Veo ``parameters.resolution`` for usage and cost tracking."""
|
||||
res = parameters.get("resolution")
|
||||
if res is None or res == "":
|
||||
return None
|
||||
return str(res).strip().lower()
|
||||
|
||||
|
||||
class GeminiVideoConfig(BaseVideoConfig):
|
||||
"""
|
||||
Configuration class for Gemini (Veo) video generation.
|
||||
|
|
@ -65,6 +75,13 @@ class GeminiVideoConfig(BaseVideoConfig):
|
|||
4. Download video using file API
|
||||
"""
|
||||
|
||||
_OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: Dict[str, str] = {
|
||||
"1280x720": "16:9",
|
||||
"1920x1080": "16:9",
|
||||
"720x1280": "9:16",
|
||||
"1080x1920": "9:16",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
|
@ -88,6 +105,8 @@ class GeminiVideoConfig(BaseVideoConfig):
|
|||
- prompt → prompt
|
||||
- input_reference → image
|
||||
- size → aspectRatio (e.g., "1280x720" → "16:9")
|
||||
- size → resolution when inferable ("1280x720"/"720x1280" → "720p",
|
||||
"1920x1080"/"1080x1920" → "1080p"); skipped if ``resolution`` is already set
|
||||
- seconds → durationSeconds (defaults to 4 seconds if not provided)
|
||||
|
||||
All other params are passed through as-is to support Gemini-specific parameters.
|
||||
|
|
@ -113,6 +132,10 @@ class GeminiVideoConfig(BaseVideoConfig):
|
|||
aspect_ratio = self._convert_size_to_aspect_ratio(size)
|
||||
if aspect_ratio:
|
||||
mapped_params["aspectRatio"] = aspect_ratio
|
||||
if not video_create_optional_params.get("resolution"):
|
||||
inferred_resolution = self._convert_size_to_resolution(size)
|
||||
if inferred_resolution is not None:
|
||||
mapped_params["resolution"] = inferred_resolution
|
||||
|
||||
# Map seconds to durationSeconds, default to 4 seconds (matching OpenAI)
|
||||
if "seconds" in video_create_optional_params:
|
||||
|
|
@ -143,14 +166,27 @@ class GeminiVideoConfig(BaseVideoConfig):
|
|||
if not size:
|
||||
return None
|
||||
|
||||
aspect_ratio_map = {
|
||||
"1280x720": "16:9",
|
||||
"1920x1080": "16:9",
|
||||
"720x1280": "9:16",
|
||||
"1080x1920": "9:16",
|
||||
}
|
||||
return self._OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO.get(size, "16:9")
|
||||
|
||||
return aspect_ratio_map.get(size, "16:9")
|
||||
def _convert_size_to_resolution(self, size: str) -> Optional[str]:
|
||||
"""
|
||||
Map OpenAI ``size`` (WxH) to Veo ``resolution`` for presets in
|
||||
``_OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO`` (720p / 1080p from the smaller edge).
|
||||
|
||||
Unknown sizes return None so the API default applies (no forced resolution).
|
||||
"""
|
||||
if not size or size not in self._OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO:
|
||||
return None
|
||||
try:
|
||||
w_str, h_str = size.split("x", 1)
|
||||
smaller = min(int(w_str), int(h_str))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if smaller == 720:
|
||||
return "720p"
|
||||
if smaller == 1080:
|
||||
return "1080p"
|
||||
return None
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
|
|
@ -279,7 +315,7 @@ class GeminiVideoConfig(BaseVideoConfig):
|
|||
We return this as a VideoObject with:
|
||||
- id: operation name (used for polling)
|
||||
- status: "processing"
|
||||
- usage: includes duration_seconds for cost calculation
|
||||
- usage: includes duration_seconds and optional video_resolution for cost calculation
|
||||
"""
|
||||
response_data = raw_response.json()
|
||||
|
||||
|
|
@ -319,6 +355,9 @@ class GeminiVideoConfig(BaseVideoConfig):
|
|||
usage_data["duration_seconds"] = float(duration)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
video_resolution = _usage_video_resolution_from_parameters(parameters)
|
||||
if video_resolution is not None:
|
||||
usage_data["video_resolution"] = video_resolution
|
||||
|
||||
video_obj.usage = usage_data
|
||||
return video_obj
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Helper util for handling openai-specific cost calculation
|
|||
- e.g.: prompt caching
|
||||
"""
|
||||
|
||||
from typing import Literal, Optional, Tuple
|
||||
from typing import Any, Literal, Mapping, Optional, Tuple
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token
|
||||
|
|
@ -128,11 +128,48 @@ def cost_per_second(
|
|||
return prompt_cost, completion_cost
|
||||
|
||||
|
||||
def _video_resolution_to_cost_field_suffix(resolution: str) -> Optional[str]:
|
||||
"""Map usage resolution to a safe suffix for ``output_cost_per_second_<suffix>`` keys."""
|
||||
r = resolution.strip().lower()
|
||||
if not r:
|
||||
return None
|
||||
safe = "".join(c for c in r if c.isalnum() or c == "_")
|
||||
if not safe or len(safe) > 24:
|
||||
return None
|
||||
return safe
|
||||
|
||||
|
||||
def _video_output_cost_per_second(
|
||||
model_info: Mapping[str, Any],
|
||||
video_resolution: Optional[str],
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
Per-second video output rate from model_info.
|
||||
|
||||
If ``video_resolution`` is set (e.g. ``1080p``, ``720p``, ``4k``), looks up
|
||||
``output_cost_per_second_<resolution>`` first (e.g. ``output_cost_per_second_1080p``),
|
||||
then falls back to ``output_cost_per_second``.
|
||||
"""
|
||||
r = (video_resolution or "").strip().lower()
|
||||
if r:
|
||||
suffix = _video_resolution_to_cost_field_suffix(r)
|
||||
if suffix is not None:
|
||||
tier_key = f"output_cost_per_second_{suffix}"
|
||||
tier_rate = model_info.get(tier_key)
|
||||
if tier_rate is not None:
|
||||
return float(tier_rate)
|
||||
out = model_info.get("output_cost_per_second")
|
||||
if out is not None:
|
||||
return float(out)
|
||||
return None
|
||||
|
||||
|
||||
def video_generation_cost(
|
||||
model: str,
|
||||
duration_seconds: float,
|
||||
custom_llm_provider: Optional[str] = None,
|
||||
model_info: Optional[ModelInfo] = None,
|
||||
video_resolution: Optional[str] = None,
|
||||
) -> float:
|
||||
"""
|
||||
Calculates the cost for video generation based on duration in seconds.
|
||||
|
|
@ -144,6 +181,7 @@ def video_generation_cost(
|
|||
- model_info: Optional[dict], deployment-level model info containing
|
||||
custom video pricing. When provided, skips the global
|
||||
get_model_info() lookup so that deployment-specific pricing is used.
|
||||
- video_resolution: Optional resolution label from usage (e.g. ``720p``, ``1080p``).
|
||||
|
||||
Returns:
|
||||
float - total_cost_in_usd
|
||||
|
|
@ -162,8 +200,7 @@ def video_generation_cost(
|
|||
)
|
||||
return video_cost_per_second * duration_seconds
|
||||
|
||||
# Fallback to general output cost per second
|
||||
output_cost_per_second = model_info.get("output_cost_per_second")
|
||||
output_cost_per_second = _video_output_cost_per_second(model_info, video_resolution)
|
||||
if output_cost_per_second is not None:
|
||||
verbose_logger.debug(
|
||||
f"For model={model} - output_cost_per_second: {output_cost_per_second}; duration: {duration_seconds}"
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase):
|
|||
We return this as a VideoObject with:
|
||||
- id: operation name (used for polling)
|
||||
- status: "processing"
|
||||
- usage: includes duration_seconds for cost calculation
|
||||
- usage: includes duration_seconds and optional video_resolution for cost calculation
|
||||
"""
|
||||
response_data = raw_response.json()
|
||||
|
||||
|
|
@ -375,6 +375,9 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase):
|
|||
usage_data["duration_seconds"] = float(duration)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
res = parameters.get("resolution")
|
||||
if res is not None and str(res).strip() != "":
|
||||
usage_data["video_resolution"] = str(res).strip().lower()
|
||||
|
||||
video_obj.usage = usage_data
|
||||
return video_obj
|
||||
|
|
|
|||
|
|
@ -15977,6 +15977,21 @@
|
|||
"video"
|
||||
]
|
||||
},
|
||||
"gemini/veo-3.1-lite-generate-preview": {
|
||||
"litellm_provider": "gemini",
|
||||
"max_input_tokens": 1024,
|
||||
"max_tokens": 1024,
|
||||
"mode": "video_generation",
|
||||
"output_cost_per_second": 0.05,
|
||||
"output_cost_per_second_1080p": 0.08,
|
||||
"source": "https://ai.google.dev/gemini-api/docs/video",
|
||||
"supported_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"gemini/veo-3.1-fast-generate-001": {
|
||||
"litellm_provider": "gemini",
|
||||
"max_input_tokens": 1024,
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ class LiteLLMParamsTypedDict(TypedDict, total=False):
|
|||
output_cost_per_token: Optional[float]
|
||||
input_cost_per_second: Optional[float]
|
||||
output_cost_per_second: Optional[float]
|
||||
output_cost_per_second_1080p: Optional[float]
|
||||
num_retries: Optional[int]
|
||||
## MOCK RESPONSES ##
|
||||
mock_response: Optional[Union[str, ModelResponse, Exception]]
|
||||
|
|
|
|||
|
|
@ -232,6 +232,9 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
|
|||
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
|
||||
output_cost_per_second: Optional[float] # for OpenAI Speech models
|
||||
output_cost_per_second_1080p: Optional[
|
||||
float
|
||||
] # video_generation tier: key output_cost_per_second_<resolution> (e.g. 1080p, 720p)
|
||||
ocr_cost_per_page: Optional[float] # for OCR models
|
||||
annotation_cost_per_page: Optional[float] # for OCR models
|
||||
search_context_cost_per_query: Optional[
|
||||
|
|
@ -2962,6 +2965,7 @@ class CustomPricingLiteLLMParams(BaseModel):
|
|||
output_cost_per_token: Optional[float] = None
|
||||
input_cost_per_second: Optional[float] = None
|
||||
output_cost_per_second: Optional[float] = None
|
||||
output_cost_per_second_1080p: Optional[float] = None
|
||||
input_cost_per_pixel: Optional[float] = None
|
||||
output_cost_per_pixel: Optional[float] = None
|
||||
|
||||
|
|
|
|||
|
|
@ -5812,6 +5812,9 @@ def _get_model_info_helper( # noqa: PLR0915
|
|||
"output_cost_per_token_above_272k_tokens", None
|
||||
),
|
||||
output_cost_per_second=_model_info.get("output_cost_per_second", None),
|
||||
output_cost_per_second_1080p=_model_info.get(
|
||||
"output_cost_per_second_1080p", None
|
||||
),
|
||||
output_cost_per_video_per_second=_model_info.get(
|
||||
"output_cost_per_video_per_second", None
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15977,6 +15977,21 @@
|
|||
"video"
|
||||
]
|
||||
},
|
||||
"gemini/veo-3.1-lite-generate-preview": {
|
||||
"litellm_provider": "gemini",
|
||||
"max_input_tokens": 1024,
|
||||
"max_tokens": 1024,
|
||||
"mode": "video_generation",
|
||||
"output_cost_per_second": 0.05,
|
||||
"output_cost_per_second_1080p": 0.08,
|
||||
"source": "https://ai.google.dev/gemini-api/docs/video",
|
||||
"supported_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"gemini/veo-3.1-fast-generate-001": {
|
||||
"litellm_provider": "gemini",
|
||||
"max_input_tokens": 1024,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class TestGeminiVideoConfig:
|
|||
def test_get_supported_openai_params(self):
|
||||
"""Test that correct params are supported."""
|
||||
params = self.config.get_supported_openai_params("veo-3.0-generate-preview")
|
||||
|
||||
|
||||
assert "model" in params
|
||||
assert "prompt" in params
|
||||
assert "input_reference" in params
|
||||
|
|
@ -38,24 +38,24 @@ class TestGeminiVideoConfig:
|
|||
result = self.config.validate_environment(
|
||||
headers=headers,
|
||||
model="veo-3.0-generate-preview",
|
||||
api_key="test-api-key-123"
|
||||
api_key="test-api-key-123",
|
||||
)
|
||||
|
||||
|
||||
assert "x-goog-api-key" in result
|
||||
assert result["x-goog-api-key"] == "test-api-key-123"
|
||||
assert "Content-Type" in result
|
||||
assert result["Content-Type"] == "application/json"
|
||||
|
||||
@patch.dict('os.environ', {}, clear=True)
|
||||
@patch.dict("os.environ", {}, clear=True)
|
||||
def test_validate_environment_missing_api_key(self):
|
||||
"""Test that missing API key raises error."""
|
||||
headers = {}
|
||||
|
||||
with pytest.raises(ValueError, match="GEMINI_API_KEY or GOOGLE_API_KEY is required"):
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="GEMINI_API_KEY or GOOGLE_API_KEY is required"
|
||||
):
|
||||
self.config.validate_environment(
|
||||
headers=headers,
|
||||
model="veo-3.0-generate-preview",
|
||||
api_key=None
|
||||
headers=headers, model="veo-3.0-generate-preview", api_key=None
|
||||
)
|
||||
|
||||
def test_get_complete_url(self):
|
||||
|
|
@ -63,20 +63,18 @@ class TestGeminiVideoConfig:
|
|||
url = self.config.get_complete_url(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
api_base="https://generativelanguage.googleapis.com",
|
||||
litellm_params={}
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
|
||||
expected = "https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning"
|
||||
assert url == expected
|
||||
|
||||
def test_get_complete_url_default_api_base(self):
|
||||
"""Test URL construction with default API base."""
|
||||
url = self.config.get_complete_url(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
api_base=None,
|
||||
litellm_params={}
|
||||
model="gemini/veo-3.0-generate-preview", api_base=None, litellm_params={}
|
||||
)
|
||||
|
||||
|
||||
assert url.startswith("https://generativelanguage.googleapis.com")
|
||||
assert "veo-3.0-generate-preview:predictLongRunning" in url
|
||||
|
||||
|
|
@ -84,32 +82,32 @@ class TestGeminiVideoConfig:
|
|||
"""Test transformation of video creation request."""
|
||||
prompt = "A cat playing with a ball of yarn"
|
||||
api_base = "https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning"
|
||||
|
||||
|
||||
data, files, url = self.config.transform_video_create_request(
|
||||
model="veo-3.0-generate-preview",
|
||||
prompt=prompt,
|
||||
api_base=api_base,
|
||||
video_create_optional_request_params={},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
# Check Veo format
|
||||
assert "instances" in data
|
||||
assert len(data["instances"]) == 1
|
||||
assert data["instances"][0]["prompt"] == prompt
|
||||
|
||||
|
||||
# Check no files are uploaded
|
||||
assert files == []
|
||||
|
||||
|
||||
# URL should be returned as-is for Gemini
|
||||
assert url == api_base
|
||||
|
||||
|
||||
def test_transform_video_create_request_with_params(self):
|
||||
"""Test transformation with optional parameters."""
|
||||
prompt = "A cat playing with a ball of yarn"
|
||||
api_base = "https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning"
|
||||
|
||||
|
||||
data, files, url = self.config.transform_video_create_request(
|
||||
model="veo-3.0-generate-preview",
|
||||
prompt=prompt,
|
||||
|
|
@ -117,38 +115,39 @@ class TestGeminiVideoConfig:
|
|||
video_create_optional_request_params={
|
||||
"aspectRatio": "16:9",
|
||||
"durationSeconds": 8,
|
||||
"resolution": "1080p"
|
||||
"resolution": "1080p",
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
# Check Veo format with instances and parameters separated
|
||||
instance = data["instances"][0]
|
||||
assert instance["prompt"] == prompt
|
||||
|
||||
|
||||
# Parameters should be in a separate object
|
||||
assert "parameters" in data
|
||||
assert data["parameters"]["aspectRatio"] == "16:9"
|
||||
assert data["parameters"]["durationSeconds"] == 8
|
||||
assert data["parameters"]["resolution"] == "1080p"
|
||||
|
||||
|
||||
def test_map_openai_params(self):
|
||||
"""Test parameter mapping from OpenAI format to Veo format."""
|
||||
openai_params = {
|
||||
"size": "1280x720",
|
||||
"seconds": "8",
|
||||
"input_reference": "test_image.jpg"
|
||||
"input_reference": "test_image.jpg",
|
||||
}
|
||||
|
||||
|
||||
mapped = self.config.map_openai_params(
|
||||
video_create_optional_params=openai_params,
|
||||
model="veo-3.0-generate-preview",
|
||||
drop_params=False
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
# Check mappings (prompt is not mapped, it's passed separately)
|
||||
assert mapped["aspectRatio"] == "16:9" # 1280x720 is landscape
|
||||
assert mapped["resolution"] == "720p"
|
||||
assert mapped["durationSeconds"] == 8
|
||||
assert mapped["image"] == "test_image.jpg"
|
||||
|
||||
|
|
@ -157,14 +156,15 @@ class TestGeminiVideoConfig:
|
|||
openai_params = {
|
||||
"size": "1280x720",
|
||||
}
|
||||
|
||||
|
||||
mapped = self.config.map_openai_params(
|
||||
video_create_optional_params=openai_params,
|
||||
model="veo-3.0-generate-preview",
|
||||
drop_params=False
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
assert mapped["aspectRatio"] == "16:9"
|
||||
assert mapped["resolution"] == "720p"
|
||||
assert "durationSeconds" not in mapped
|
||||
|
||||
def test_map_openai_params_with_gemini_specific_params(self):
|
||||
|
|
@ -175,19 +175,20 @@ class TestGeminiVideoConfig:
|
|||
"video": {"bytesBase64Encoded": "abc123", "mimeType": "video/mp4"},
|
||||
"negativePrompt": "no people",
|
||||
"referenceImages": [{"bytesBase64Encoded": "xyz789"}],
|
||||
"personGeneration": "allow"
|
||||
"personGeneration": "allow",
|
||||
}
|
||||
|
||||
|
||||
mapped = self.config.map_openai_params(
|
||||
video_create_optional_params=params_with_gemini_specific,
|
||||
model="veo-3.1-generate-preview",
|
||||
drop_params=False
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
# Check OpenAI params are mapped
|
||||
assert mapped["aspectRatio"] == "16:9"
|
||||
assert mapped["resolution"] == "720p"
|
||||
assert mapped["durationSeconds"] == 8
|
||||
|
||||
|
||||
# Check Gemini-specific params are passed through
|
||||
assert "video" in mapped
|
||||
assert mapped["video"]["bytesBase64Encoded"] == "abc123"
|
||||
|
|
@ -198,73 +199,106 @@ class TestGeminiVideoConfig:
|
|||
def test_map_openai_params_with_extra_body(self):
|
||||
"""Test that extra_body params are merged and extra_body is removed."""
|
||||
from litellm.videos.utils import VideoGenerationRequestUtils
|
||||
|
||||
|
||||
params_with_extra_body = {
|
||||
"seconds": "4",
|
||||
"extra_body": {
|
||||
"negativePrompt": "no people",
|
||||
"personGeneration": "allow",
|
||||
"resolution": "1080p"
|
||||
}
|
||||
"resolution": "1080p",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
mapped = VideoGenerationRequestUtils.get_optional_params_video_generation(
|
||||
model="veo-3.0-generate-preview",
|
||||
video_generation_provider_config=self.config,
|
||||
video_generation_optional_params=params_with_extra_body
|
||||
video_generation_optional_params=params_with_extra_body,
|
||||
)
|
||||
|
||||
|
||||
# Check OpenAI params are mapped
|
||||
assert mapped["durationSeconds"] == 4
|
||||
|
||||
|
||||
# Check extra_body params are merged
|
||||
assert mapped["negativePrompt"] == "no people"
|
||||
assert mapped["personGeneration"] == "allow"
|
||||
assert mapped["resolution"] == "1080p"
|
||||
|
||||
|
||||
# Check extra_body itself is removed
|
||||
assert "extra_body" not in mapped
|
||||
|
||||
|
||||
def test_convert_size_to_aspect_ratio(self):
|
||||
"""Test size to aspect ratio conversion."""
|
||||
# Landscape
|
||||
assert self.config._convert_size_to_aspect_ratio("1280x720") == "16:9"
|
||||
assert self.config._convert_size_to_aspect_ratio("1920x1080") == "16:9"
|
||||
|
||||
|
||||
# Portrait
|
||||
assert self.config._convert_size_to_aspect_ratio("720x1280") == "9:16"
|
||||
assert self.config._convert_size_to_aspect_ratio("1080x1920") == "9:16"
|
||||
|
||||
|
||||
# Invalid (defaults to 16:9)
|
||||
assert self.config._convert_size_to_aspect_ratio("invalid") == "16:9"
|
||||
# Empty string returns None (no size specified)
|
||||
assert self.config._convert_size_to_aspect_ratio("") is None
|
||||
|
||||
def test_convert_size_to_resolution(self):
|
||||
"""OpenAI WxH maps to Veo resolution when height is 720 or 1080."""
|
||||
assert self.config._convert_size_to_resolution("1280x720") == "720p"
|
||||
assert self.config._convert_size_to_resolution("720x1280") == "720p"
|
||||
assert self.config._convert_size_to_resolution("1920x1080") == "1080p"
|
||||
assert self.config._convert_size_to_resolution("1080x1920") == "1080p"
|
||||
assert self.config._convert_size_to_resolution("invalid") is None
|
||||
assert self.config._convert_size_to_resolution("") is None
|
||||
|
||||
def test_map_openai_params_size_does_not_override_explicit_resolution(self):
|
||||
"""Explicit resolution wins; size still maps aspect ratio."""
|
||||
openai_params = {
|
||||
"size": "1280x720",
|
||||
"resolution": "1080p",
|
||||
"seconds": "8",
|
||||
}
|
||||
mapped = self.config.map_openai_params(
|
||||
video_create_optional_params=openai_params,
|
||||
model="veo-3.0-generate-preview",
|
||||
drop_params=False,
|
||||
)
|
||||
assert mapped["aspectRatio"] == "16:9"
|
||||
assert mapped["resolution"] == "1080p"
|
||||
assert mapped["durationSeconds"] == 8
|
||||
|
||||
def test_map_openai_params_1080p_landscape_size(self):
|
||||
openai_params = {"size": "1920x1080", "seconds": "8"}
|
||||
mapped = self.config.map_openai_params(
|
||||
video_create_optional_params=openai_params,
|
||||
model="veo-3.0-generate-preview",
|
||||
drop_params=False,
|
||||
)
|
||||
assert mapped["aspectRatio"] == "16:9"
|
||||
assert mapped["resolution"] == "1080p"
|
||||
assert mapped["durationSeconds"] == 8
|
||||
|
||||
def test_transform_video_create_response(self):
|
||||
"""Test transformation of video creation response."""
|
||||
# Mock response
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_1234567890",
|
||||
"metadata": {
|
||||
"createTime": "2024-11-04T10:00:00.123456Z"
|
||||
}
|
||||
"metadata": {"createTime": "2024-11-04T10:00:00.123456Z"},
|
||||
}
|
||||
|
||||
|
||||
result = self.config.transform_video_create_response(
|
||||
model="veo-3.0-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini"
|
||||
custom_llm_provider="gemini",
|
||||
)
|
||||
|
||||
|
||||
assert isinstance(result, VideoObject)
|
||||
# ID is base64 encoded with provider info
|
||||
assert result.id.startswith("video_")
|
||||
assert result.status == "processing"
|
||||
assert result.object == "video"
|
||||
|
||||
|
||||
def test_transform_video_create_response_with_cost_tracking(self):
|
||||
"""Test that duration is captured for cost tracking."""
|
||||
# Mock response
|
||||
|
|
@ -272,67 +306,87 @@ class TestGeminiVideoConfig:
|
|||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_1234567890",
|
||||
}
|
||||
|
||||
|
||||
# Request data with durationSeconds in parameters
|
||||
request_data = {
|
||||
"instances": [{"prompt": "A test video"}],
|
||||
"parameters": {
|
||||
"durationSeconds": 5,
|
||||
"aspectRatio": "16:9"
|
||||
}
|
||||
"parameters": {"durationSeconds": 5, "aspectRatio": "16:9"},
|
||||
}
|
||||
|
||||
|
||||
result = self.config.transform_video_create_response(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini",
|
||||
request_data=request_data
|
||||
request_data=request_data,
|
||||
)
|
||||
|
||||
|
||||
assert isinstance(result, VideoObject)
|
||||
assert result.usage is not None, "Usage should be set"
|
||||
assert "duration_seconds" in result.usage, "duration_seconds should be in usage"
|
||||
assert result.usage["duration_seconds"] == 5.0, f"Expected 5.0, got {result.usage['duration_seconds']}"
|
||||
assert (
|
||||
result.usage["duration_seconds"] == 5.0
|
||||
), f"Expected 5.0, got {result.usage['duration_seconds']}"
|
||||
|
||||
def test_transform_video_create_response_cost_tracking_with_different_durations(self):
|
||||
def test_transform_video_create_response_usage_includes_video_resolution(self):
|
||||
"""Resolution from request parameters is copied into usage for cost tracking."""
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.json.return_value = {"name": "operations/generate_1234567890"}
|
||||
request_data = {
|
||||
"instances": [{"prompt": "Test"}],
|
||||
"parameters": {"durationSeconds": 8, "resolution": "1080P"},
|
||||
}
|
||||
result = self.config.transform_video_create_response(
|
||||
model="gemini/veo-3.1-lite-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini",
|
||||
request_data=request_data,
|
||||
)
|
||||
assert result.usage is not None
|
||||
assert result.usage["video_resolution"] == "1080p"
|
||||
assert result.usage["duration_seconds"] == 8.0
|
||||
|
||||
def test_transform_video_create_response_cost_tracking_with_different_durations(
|
||||
self,
|
||||
):
|
||||
"""Test cost tracking with different duration values."""
|
||||
# Mock response
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_1234567890",
|
||||
}
|
||||
|
||||
|
||||
# Test with 8 seconds
|
||||
request_data_8s = {
|
||||
"instances": [{"prompt": "Test"}],
|
||||
"parameters": {"durationSeconds": 8}
|
||||
"parameters": {"durationSeconds": 8},
|
||||
}
|
||||
|
||||
|
||||
result_8s = self.config.transform_video_create_response(
|
||||
model="gemini/veo-3.1-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini",
|
||||
request_data=request_data_8s
|
||||
request_data=request_data_8s,
|
||||
)
|
||||
|
||||
|
||||
assert result_8s.usage["duration_seconds"] == 8.0
|
||||
|
||||
|
||||
# Test with 4 seconds
|
||||
request_data_4s = {
|
||||
"instances": [{"prompt": "Test"}],
|
||||
"parameters": {"durationSeconds": 4}
|
||||
"parameters": {"durationSeconds": 4},
|
||||
}
|
||||
|
||||
|
||||
result_4s = self.config.transform_video_create_response(
|
||||
model="gemini/veo-3.1-fast-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini",
|
||||
request_data=request_data_4s
|
||||
request_data=request_data_4s,
|
||||
)
|
||||
|
||||
|
||||
assert result_4s.usage["duration_seconds"] == 4.0
|
||||
|
||||
def test_transform_video_create_response_cost_tracking_no_duration(self):
|
||||
|
|
@ -342,40 +396,40 @@ class TestGeminiVideoConfig:
|
|||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_1234567890",
|
||||
}
|
||||
|
||||
|
||||
# Request data without durationSeconds (should default to 8 seconds for Google Veo)
|
||||
request_data = {
|
||||
"instances": [{"prompt": "A test video"}],
|
||||
"parameters": {
|
||||
"aspectRatio": "16:9"
|
||||
}
|
||||
"parameters": {"aspectRatio": "16:9"},
|
||||
}
|
||||
|
||||
|
||||
result = self.config.transform_video_create_response(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini",
|
||||
request_data=request_data
|
||||
request_data=request_data,
|
||||
)
|
||||
|
||||
|
||||
assert isinstance(result, VideoObject)
|
||||
# When no duration is provided, it defaults to 8 seconds (Google Veo default)
|
||||
assert result.usage is not None
|
||||
assert "duration_seconds" in result.usage
|
||||
assert result.usage["duration_seconds"] == 8.0, "Should default to 8 seconds when not provided (Google Veo default)"
|
||||
assert (
|
||||
result.usage["duration_seconds"] == 8.0
|
||||
), "Should default to 8 seconds when not provided (Google Veo default)"
|
||||
|
||||
def test_transform_video_status_retrieve_request(self):
|
||||
"""Test transformation of status retrieve request."""
|
||||
video_id = "gemini::operations/generate_1234567890::veo-3.0"
|
||||
|
||||
|
||||
url, params = self.config.transform_video_status_retrieve_request(
|
||||
video_id=video_id,
|
||||
api_base="https://generativelanguage.googleapis.com",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
assert "operations/generate_1234567890" in url
|
||||
assert "v1beta" in url
|
||||
assert params == {}
|
||||
|
|
@ -386,17 +440,15 @@ class TestGeminiVideoConfig:
|
|||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_1234567890",
|
||||
"done": False,
|
||||
"metadata": {
|
||||
"createTime": "2024-11-04T10:00:00.123456Z"
|
||||
}
|
||||
"metadata": {"createTime": "2024-11-04T10:00:00.123456Z"},
|
||||
}
|
||||
|
||||
|
||||
result = self.config.transform_video_status_retrieve_response(
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini"
|
||||
custom_llm_provider="gemini",
|
||||
)
|
||||
|
||||
|
||||
assert isinstance(result, VideoObject)
|
||||
assert result.status == "processing"
|
||||
|
||||
|
|
@ -406,36 +458,28 @@ class TestGeminiVideoConfig:
|
|||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_1234567890",
|
||||
"done": True,
|
||||
"metadata": {
|
||||
"createTime": "2024-11-04T10:00:00.123456Z"
|
||||
},
|
||||
"metadata": {"createTime": "2024-11-04T10:00:00.123456Z"},
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": "files/abc123xyz"
|
||||
}
|
||||
}
|
||||
]
|
||||
"generatedSamples": [{"video": {"uri": "files/abc123xyz"}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
result = self.config.transform_video_status_retrieve_response(
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj,
|
||||
custom_llm_provider="gemini"
|
||||
custom_llm_provider="gemini",
|
||||
)
|
||||
|
||||
|
||||
assert isinstance(result, VideoObject)
|
||||
assert result.status == "completed"
|
||||
|
||||
@patch('litellm.module_level_client')
|
||||
@patch("litellm.module_level_client")
|
||||
def test_transform_video_content_request(self, mock_client):
|
||||
"""Test transformation of content download request."""
|
||||
video_id = "gemini::operations/generate_1234567890::veo-3.0"
|
||||
|
||||
|
||||
# Mock the status response
|
||||
mock_status_response = Mock(spec=httpx.Response)
|
||||
mock_status_response.json.return_value = {
|
||||
|
|
@ -443,26 +487,20 @@ class TestGeminiVideoConfig:
|
|||
"done": True,
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": "files/abc123xyz"
|
||||
}
|
||||
}
|
||||
]
|
||||
"generatedSamples": [{"video": {"uri": "files/abc123xyz"}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
mock_status_response.raise_for_status = Mock()
|
||||
mock_client.get.return_value = mock_status_response
|
||||
|
||||
|
||||
url, params = self.config.transform_video_content_request(
|
||||
video_id=video_id,
|
||||
api_base="https://generativelanguage.googleapis.com",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
# Should return download URL (may or may not include :download suffix)
|
||||
assert "files/abc123xyz" in url
|
||||
# Params are empty for Gemini file URIs
|
||||
|
|
@ -471,16 +509,13 @@ class TestGeminiVideoConfig:
|
|||
def test_transform_video_content_response_bytes(self):
|
||||
"""Test transformation of content response (returns bytes directly)."""
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.headers = httpx.Headers({
|
||||
"content-type": "video/mp4"
|
||||
})
|
||||
mock_response.headers = httpx.Headers({"content-type": "video/mp4"})
|
||||
mock_response.content = b"fake_video_data"
|
||||
|
||||
|
||||
result = self.config.transform_video_content_response(
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.mock_logging_obj
|
||||
raw_response=mock_response, logging_obj=self.mock_logging_obj
|
||||
)
|
||||
|
||||
|
||||
assert result == b"fake_video_data"
|
||||
|
||||
def test_video_remix_not_supported(self):
|
||||
|
|
@ -491,7 +526,7 @@ class TestGeminiVideoConfig:
|
|||
prompt="test prompt",
|
||||
api_base="https://test.com",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
def test_video_list_not_supported(self):
|
||||
|
|
@ -500,7 +535,7 @@ class TestGeminiVideoConfig:
|
|||
self.config.transform_video_list_request(
|
||||
api_base="https://test.com",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
def test_video_delete_not_supported(self):
|
||||
|
|
@ -510,7 +545,7 @@ class TestGeminiVideoConfig:
|
|||
video_id="test_id",
|
||||
api_base="https://test.com",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -521,7 +556,7 @@ class TestGeminiVideoIntegration:
|
|||
"""Test full workflow with mocked responses."""
|
||||
config = GeminiVideoConfig()
|
||||
mock_logging_obj = Mock()
|
||||
|
||||
|
||||
# Step 1: Create request with parameters
|
||||
prompt = "A beautiful sunset over mountains"
|
||||
api_base = "https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning"
|
||||
|
|
@ -531,69 +566,59 @@ class TestGeminiVideoIntegration:
|
|||
api_base=api_base,
|
||||
video_create_optional_request_params={
|
||||
"aspectRatio": "16:9",
|
||||
"durationSeconds": 8
|
||||
"durationSeconds": 8,
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={}
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
# Verify instances and parameters structure
|
||||
assert data["instances"][0]["prompt"] == prompt
|
||||
assert data["parameters"]["aspectRatio"] == "16:9"
|
||||
assert data["parameters"]["durationSeconds"] == 8
|
||||
|
||||
|
||||
# Step 2: Parse create response
|
||||
mock_create_response = Mock(spec=httpx.Response)
|
||||
mock_create_response.json.return_value = {
|
||||
"name": "operations/generate_abc123",
|
||||
"metadata": {
|
||||
"createTime": "2024-11-04T10:00:00.123456Z"
|
||||
}
|
||||
"metadata": {"createTime": "2024-11-04T10:00:00.123456Z"},
|
||||
}
|
||||
|
||||
|
||||
video_obj = config.transform_video_create_response(
|
||||
model="veo-3.0-generate-preview",
|
||||
raw_response=mock_create_response,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="gemini"
|
||||
custom_llm_provider="gemini",
|
||||
)
|
||||
|
||||
|
||||
assert video_obj.status == "processing"
|
||||
assert video_obj.id.startswith("video_")
|
||||
|
||||
|
||||
# Step 3: Check status (completed)
|
||||
mock_status_response = Mock(spec=httpx.Response)
|
||||
mock_status_response.json.return_value = {
|
||||
"name": "operations/generate_abc123",
|
||||
"done": True,
|
||||
"metadata": {
|
||||
"createTime": "2024-11-04T10:00:00.123456Z"
|
||||
},
|
||||
"metadata": {"createTime": "2024-11-04T10:00:00.123456Z"},
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": "files/video123"
|
||||
}
|
||||
}
|
||||
]
|
||||
"generatedSamples": [{"video": {"uri": "files/video123"}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
status_obj = config.transform_video_status_retrieve_response(
|
||||
raw_response=mock_status_response,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="gemini"
|
||||
custom_llm_provider="gemini",
|
||||
)
|
||||
|
||||
|
||||
assert status_obj.status == "completed"
|
||||
|
||||
|
||||
class TestGeminiVideoCostTracking:
|
||||
"""Test cost tracking for Gemini video generation."""
|
||||
|
||||
|
||||
def test_cost_calculation_with_duration(self):
|
||||
"""Test that cost is calculated correctly using duration from usage."""
|
||||
# Test VEO 2.0 ($0.35/second)
|
||||
|
|
@ -604,8 +629,10 @@ class TestGeminiVideoCostTracking:
|
|||
model_info={"output_cost_per_second": 0.35},
|
||||
)
|
||||
expected_veo2 = 0.35 * 5.0 # $1.75
|
||||
assert abs(cost_veo2 - expected_veo2) < 0.001, f"Expected ${expected_veo2}, got ${cost_veo2}"
|
||||
|
||||
assert (
|
||||
abs(cost_veo2 - expected_veo2) < 0.001
|
||||
), f"Expected ${expected_veo2}, got ${cost_veo2}"
|
||||
|
||||
# Test VEO 3.0 ($0.75/second)
|
||||
cost_veo3 = video_generation_cost(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
|
|
@ -614,8 +641,10 @@ class TestGeminiVideoCostTracking:
|
|||
model_info={"output_cost_per_second": 0.75},
|
||||
)
|
||||
expected_veo3 = 0.75 * 8.0 # $6.00
|
||||
assert abs(cost_veo3 - expected_veo3) < 0.001, f"Expected ${expected_veo3}, got ${cost_veo3}"
|
||||
|
||||
assert (
|
||||
abs(cost_veo3 - expected_veo3) < 0.001
|
||||
), f"Expected ${expected_veo3}, got ${cost_veo3}"
|
||||
|
||||
# Test VEO 3.1 Standard ($0.40/second)
|
||||
cost_veo31 = video_generation_cost(
|
||||
model="gemini/veo-3.1-generate-preview",
|
||||
|
|
@ -624,8 +653,10 @@ class TestGeminiVideoCostTracking:
|
|||
model_info={"output_cost_per_second": 0.40},
|
||||
)
|
||||
expected_veo31 = 0.40 * 10.0 # $4.00
|
||||
assert abs(cost_veo31 - expected_veo31) < 0.001, f"Expected ${expected_veo31}, got ${cost_veo31}"
|
||||
|
||||
assert (
|
||||
abs(cost_veo31 - expected_veo31) < 0.001
|
||||
), f"Expected ${expected_veo31}, got ${cost_veo31}"
|
||||
|
||||
# Test VEO 3.1 Fast ($0.15/second)
|
||||
cost_veo31_fast = video_generation_cost(
|
||||
model="gemini/veo-3.1-fast-generate-preview",
|
||||
|
|
@ -634,39 +665,64 @@ class TestGeminiVideoCostTracking:
|
|||
model_info={"output_cost_per_second": 0.15},
|
||||
)
|
||||
expected_veo31_fast = 0.15 * 6.0 # $0.90
|
||||
assert abs(cost_veo31_fast - expected_veo31_fast) < 0.001, f"Expected ${expected_veo31_fast}, got ${cost_veo31_fast}"
|
||||
|
||||
assert (
|
||||
abs(cost_veo31_fast - expected_veo31_fast) < 0.001
|
||||
), f"Expected ${expected_veo31_fast}, got ${cost_veo31_fast}"
|
||||
|
||||
def test_cost_calculation_veo_lite_1080p_tier(self):
|
||||
"""Veo 3.1 Lite uses output_cost_per_second_1080p when video_resolution is 1080p."""
|
||||
model_info = {
|
||||
"output_cost_per_second": 0.05,
|
||||
"output_cost_per_second_1080p": 0.08,
|
||||
}
|
||||
cost_720 = video_generation_cost(
|
||||
model="gemini/veo-3.1-lite-generate-preview",
|
||||
duration_seconds=10.0,
|
||||
custom_llm_provider="gemini",
|
||||
model_info=model_info,
|
||||
video_resolution="720p",
|
||||
)
|
||||
cost_1080 = video_generation_cost(
|
||||
model="gemini/veo-3.1-lite-generate-preview",
|
||||
duration_seconds=10.0,
|
||||
custom_llm_provider="gemini",
|
||||
model_info=model_info,
|
||||
video_resolution="1080p",
|
||||
)
|
||||
assert abs(cost_720 - 0.5) < 0.001
|
||||
assert abs(cost_1080 - 0.8) < 0.001
|
||||
|
||||
def test_cost_calculation_end_to_end(self):
|
||||
"""Test complete cost tracking flow: request -> response -> cost calculation."""
|
||||
config = GeminiVideoConfig()
|
||||
mock_logging_obj = Mock()
|
||||
|
||||
|
||||
# Create request with duration
|
||||
request_data = {
|
||||
"instances": [{"prompt": "A beautiful sunset"}],
|
||||
"parameters": {"durationSeconds": 5}
|
||||
"parameters": {"durationSeconds": 5},
|
||||
}
|
||||
|
||||
|
||||
# Mock response
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.json.return_value = {
|
||||
"name": "operations/generate_test123",
|
||||
}
|
||||
|
||||
|
||||
# Transform response
|
||||
video_obj = config.transform_video_create_response(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
raw_response=mock_response,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="gemini",
|
||||
request_data=request_data
|
||||
request_data=request_data,
|
||||
)
|
||||
|
||||
|
||||
# Verify usage has duration
|
||||
assert video_obj.usage is not None
|
||||
assert "duration_seconds" in video_obj.usage
|
||||
duration = video_obj.usage["duration_seconds"]
|
||||
|
||||
|
||||
# Calculate cost using the duration from usage
|
||||
cost = video_generation_cost(
|
||||
model="gemini/veo-3.0-generate-preview",
|
||||
|
|
@ -674,12 +730,13 @@ class TestGeminiVideoCostTracking:
|
|||
custom_llm_provider="gemini",
|
||||
model_info={"output_cost_per_second": 0.75},
|
||||
)
|
||||
|
||||
|
||||
# Verify cost calculation (VEO 3.0 is $0.75/second)
|
||||
expected_cost = 0.75 * 5.0 # $3.75
|
||||
assert abs(cost - expected_cost) < 0.001, f"Expected ${expected_cost}, got ${cost}"
|
||||
assert (
|
||||
abs(cost - expected_cost) < 0.001
|
||||
), f"Expected ${expected_cost}, got ${cost}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue