mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
test(e2e): cover fal Seedance video create, poll and download
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
e0b455e94e
commit
85a6a8e206
5 changed files with 109 additions and 1 deletions
|
|
@ -80,6 +80,7 @@
|
|||
- {id: llm.images_generations.vertex.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: images_generations, route: vertex, capability: basic, streaming: nonstream, assertions: [works], source: "vertex_ai/image_generation/image_generation_handler.py", rationale: "Vertex Imagen"}
|
||||
- {id: llm.images_generations.bedrock.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: images_generations, route: bedrock_converse, capability: basic, streaming: nonstream, assertions: [works], source: "bedrock/image_generation/image_handler.py", rationale: "Bedrock Titan Image"}
|
||||
- {id: llm.images_generations.black_forest_labs.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: images_generations, route: openai, capability: basic, streaming: nonstream, assertions: [works], source: "black_forest_labs/image_generation/handler.py", rationale: "BFL Flux via OpenAI-compat"}
|
||||
- {id: llm.videos.fal_ai.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: videos, route: fal_ai, capability: basic, streaming: nonstream, assertions: [works], source: "test_video_generation_e2e.py", rationale: "fal queue video create, poll, content download"}
|
||||
- {id: llm.audio_speech.openai.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: audio_speech, route: openai, capability: basic, streaming: nonstream, assertions: [works], source: "test_audio_speech_e2e.py:22", rationale: "OpenAI TTS binary audio"}
|
||||
- {id: llm.audio_speech.openai.basic.stream.works, module: llm, tier: P1, subject_endpoint: audio_speech, route: openai, capability: basic, streaming: stream, assertions: [works], source: "proxy_server.py:9043", rationale: "TTS streaming chunk generator"}
|
||||
- {id: llm.audio_speech.openai.input_validation.nonstream.works, module: llm, tier: P1, subject_endpoint: audio_speech, route: openai, capability: input_validation, streaming: nonstream, assertions: [works], source: "vendor strategy §9.6 / LIT-4778", rationale: "TTS missing input/model, invalid voice, empty input rejected"}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ LlmEndpoint = Literal[
|
|||
"vector_stores",
|
||||
"ocr",
|
||||
"bedrock_native",
|
||||
"videos",
|
||||
]
|
||||
|
||||
LlmRoute = Literal[
|
||||
|
|
@ -53,6 +54,7 @@ LlmRoute = Literal[
|
|||
"bedrock_converse",
|
||||
"bedrock_invoke",
|
||||
"cohere",
|
||||
"fal_ai",
|
||||
"gemini",
|
||||
"hosted_vllm",
|
||||
"openai",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ most likely to silently break and the one a mock can't prove works.
|
|||
|----------|---------------|-----------|------------|-------------|--------|
|
||||
| Chat | live (spend suite) | live (spend suite) | gap | live | partial |
|
||||
| Embeddings | live (spend suite) | n/a | n/a | live | covered |
|
||||
| Video | live (fal.ai Seedance) | n/a | n/a | - | partial |
|
||||
| Responses / image / audio / rerank / realtime | - | - | - | - | gap |
|
||||
|
||||
## This suite's files
|
||||
|
|
@ -61,6 +62,7 @@ most likely to silently break and the one a mock can't prove works.
|
|||
| `test_anthropic_passthrough_streaming_logs_cost` | anthropic native, stream, cost |
|
||||
| `test_anthropic_passthrough_tool_call_logs_cost` | anthropic native, tool call, cost |
|
||||
| `test_vertex_passthrough_via_managed_model_logs_cost` | vertex_ai native, non-stream, cost |
|
||||
| `test_fal_seedance_video_completes_and_downloads` | fal.ai Seedance video create, poll, and content download |
|
||||
|
||||
Vertex keeps the credential on the proxy like gemini/anthropic, but the deployment is
|
||||
added at runtime instead of declared in the gateway config: the test POSTs `/model/new`
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from dataclasses import dataclass
|
|||
from typing import Literal
|
||||
|
||||
from e2e_config import SLOW_PROVIDER_TIMEOUT_SECONDS
|
||||
from e2e_http import BinaryStream, Result, StreamingResponse
|
||||
from e2e_http import BinaryStream, NoBody, Result, StreamingResponse
|
||||
from models import CacheControl, ChatMessage, LiteLLMParamsBody, RichMessage, TextBlock
|
||||
from proxy_client import ProxyClient
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -26,6 +26,8 @@ __all__ = [
|
|||
"TextBlock",
|
||||
"TranscriptionForm",
|
||||
"TranscriptionResult",
|
||||
"VideoObject",
|
||||
"VideoRequest",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -127,6 +129,13 @@ class ImageRequest(BaseModel):
|
|||
size: str = "1024x1024"
|
||||
|
||||
|
||||
class VideoRequest(BaseModel):
|
||||
model: str
|
||||
prompt: str
|
||||
seconds: str = "4"
|
||||
size: str = "1280x720"
|
||||
|
||||
|
||||
class ImageEditForm(BaseModel):
|
||||
model: str
|
||||
prompt: str
|
||||
|
|
@ -267,6 +276,12 @@ class ImagesResult(BaseModel):
|
|||
data: list[ImageItem] = []
|
||||
|
||||
|
||||
class VideoObject(BaseModel):
|
||||
id: str
|
||||
status: str
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class TranscriptionResult(BaseModel):
|
||||
text: str = ""
|
||||
|
||||
|
|
@ -440,6 +455,25 @@ class EndpointsClient:
|
|||
"/v1/images/generations", key, ImageRequest(model=model, prompt=prompt)
|
||||
)
|
||||
|
||||
def videos(self, key: str, model: str, prompt: str) -> StreamingResponse:
|
||||
return self._send(
|
||||
"/v1/videos", key, VideoRequest(model=model, prompt=prompt)
|
||||
)
|
||||
|
||||
def video_status(self, key: str, video_id: str) -> Result[VideoObject]:
|
||||
return self.proxy.transport.get(
|
||||
f"/v1/videos/{video_id}",
|
||||
headers=self.proxy.transport.bearer(key),
|
||||
params=NoBody(),
|
||||
response_type=VideoObject,
|
||||
)
|
||||
|
||||
def video_content(self, key: str, video_id: str) -> StreamingResponse:
|
||||
return self.proxy.transport.download(
|
||||
f"/v1/videos/{video_id}/content",
|
||||
headers=self.proxy.transport.bearer(key),
|
||||
)
|
||||
|
||||
def image_edit(
|
||||
self, key: str, model: str, prompt: str, image: bytes, *, filename: str = "image.png"
|
||||
) -> Result[ImagesResult]:
|
||||
|
|
|
|||
69
tests/e2e/llm_translation/test_video_generation_e2e.py
Normal file
69
tests/e2e/llm_translation/test_video_generation_e2e.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""Live e2e: POST /v1/videos creates a video and serves its content.
|
||||
|
||||
Registers a fal.ai Seedance deployment at runtime, polls the queued video until it
|
||||
completes, and asserts the generated content is returned as binary data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import require_successful_call, unwrap
|
||||
from endpoints_client import EndpointsClient, VideoObject
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
_POLL_INTERVAL_SECONDS: Final[float] = 5.0
|
||||
_POLL_TIMEOUT_SECONDS: Final[float] = 600.0
|
||||
|
||||
|
||||
def _wait_for_completion(
|
||||
endpoints_client: EndpointsClient, key: str, created: VideoObject
|
||||
) -> VideoObject:
|
||||
deadline = time.monotonic() + _POLL_TIMEOUT_SECONDS
|
||||
while time.monotonic() < deadline:
|
||||
status = unwrap(endpoints_client.video_status(key, created.id))
|
||||
assert status.id == created.id
|
||||
if status.status == "completed":
|
||||
return status
|
||||
if status.status == "failed":
|
||||
pytest.fail(f"fal.ai video generation failed: {status}")
|
||||
time.sleep(_POLL_INTERVAL_SECONDS)
|
||||
pytest.fail(f"fal.ai video {created.id!r} did not complete within {_POLL_TIMEOUT_SECONDS}s")
|
||||
|
||||
|
||||
class TestVideoGeneration:
|
||||
@pytest.mark.covers("llm.videos.fal_ai.basic.nonstream.works")
|
||||
def test_fal_seedance_video_completes_and_downloads(
|
||||
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
||||
) -> None:
|
||||
model = f"e2e-fal-video-{unique_marker()}"
|
||||
model_id = endpoints_client.create_model(
|
||||
model,
|
||||
LiteLLMParamsBody(
|
||||
model="fal_ai/bytedance/seedance-2.5/text-to-video",
|
||||
api_key="os.environ/FAL_AI_API_KEY",
|
||||
),
|
||||
)
|
||||
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
||||
key = resources.key()
|
||||
|
||||
result = endpoints_client.videos(
|
||||
key, model, "a red fox running through snow at dawn"
|
||||
)
|
||||
require_successful_call(result)
|
||||
created = VideoObject.model_validate_json(result.body)
|
||||
assert created.id
|
||||
assert created.model
|
||||
|
||||
_wait_for_completion(endpoints_client, key, created)
|
||||
|
||||
content = endpoints_client.video_content(key, created.id)
|
||||
require_successful_call(content)
|
||||
assert len(content.body) > 0
|
||||
assert not (content.content_type or "").startswith("application/json")
|
||||
Loading…
Add table
Reference in a new issue