mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-30 06:43:27 +00:00
fix(xai): bill video creation for the submitted duration and resolution
The xAI create response only carries request_id, so usage was empty and completion_cost priced every created video at zero seconds. Derive usage.duration_seconds and usage.video_resolution from the request body sent to xAI, and forward the resolution param that was being dropped. Also cover the remaining image edit/generation, video, and ownership behaviors that had no tests.
This commit is contained in:
parent
3c12c514dd
commit
2c313b6fd0
9 changed files with 397 additions and 18 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import math
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
|
@ -52,6 +53,28 @@ def _duration_from_seconds(seconds: object) -> int:
|
|||
return 6
|
||||
|
||||
|
||||
def _positive_seconds(value: object) -> float | None:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
|
||||
return None
|
||||
try:
|
||||
seconds: Final = float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return seconds if math.isfinite(seconds) and seconds > 0 else None
|
||||
|
||||
|
||||
def _requested_video_usage(request_data: Mapping[str, object]) -> dict[str, object]: # mutable-ok: VideoObject.usage
|
||||
resolution: Final = request_data.get("resolution") or request_data.get("resolution_name")
|
||||
return {
|
||||
key: value
|
||||
for key, value in (
|
||||
("duration_seconds", _positive_seconds(request_data.get("duration"))),
|
||||
("video_resolution", resolution if isinstance(resolution, str) and resolution else None),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
|
||||
|
||||
_STATUS_MAP: Final = {
|
||||
"done": "completed",
|
||||
"completed": "completed",
|
||||
|
|
@ -206,6 +229,7 @@ class XAIVideoConfig(BaseVideoConfig):
|
|||
"image",
|
||||
"images",
|
||||
"duration",
|
||||
"resolution",
|
||||
"resolution_name",
|
||||
"aspect_ratio",
|
||||
"size",
|
||||
|
|
@ -238,7 +262,9 @@ class XAIVideoConfig(BaseVideoConfig):
|
|||
if not request_id:
|
||||
raise ValueError(f"xAI video generation response missing request_id: {response_data}")
|
||||
|
||||
usage: Final = response_data.get("usage") or {}
|
||||
provider_usage: Final[object] = response_data.get("usage")
|
||||
requested_usage: Final = _requested_video_usage(request_data or {})
|
||||
duration_seconds: Final = requested_usage.get("duration_seconds")
|
||||
video_obj: Final = VideoObject(
|
||||
id=str(request_id),
|
||||
object="video",
|
||||
|
|
@ -246,11 +272,11 @@ class XAIVideoConfig(BaseVideoConfig):
|
|||
created_at=int(time.time()),
|
||||
model=XAIModelInfo.get_base_model(model) or model,
|
||||
progress=0,
|
||||
seconds=f"{duration_seconds:g}" if isinstance(duration_seconds, float) else None,
|
||||
)
|
||||
if custom_llm_provider:
|
||||
video_obj.id = encode_video_id_with_provider(video_obj.id, custom_llm_provider, model)
|
||||
usage_body: Final = usage if isinstance(usage, dict) else None
|
||||
video_obj.usage = usage_body or {}
|
||||
video_obj.usage = {**requested_usage, **(provider_usage if _is_json_object(provider_usage) else {})}
|
||||
return video_obj
|
||||
|
||||
def _video_resource_url(self, api_base: str, video_id: str) -> str:
|
||||
|
|
@ -301,7 +327,7 @@ class XAIVideoConfig(BaseVideoConfig):
|
|||
seconds=seconds,
|
||||
usage=response_data.get("usage") if isinstance(response_data.get("usage"), dict) else {},
|
||||
)
|
||||
video_obj._hidden_params["video_url"] = video_url # pyright: ignore[reportPrivateUsage] # no public setter
|
||||
video_obj.hidden_params["video_url"] = video_url
|
||||
if custom_llm_provider and video_obj.id and video_obj.id != "unknown":
|
||||
video_obj.id = encode_video_id_with_provider(video_obj.id, custom_llm_provider, response_data.get("model"))
|
||||
return video_obj
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from litellm.llms.xai.batches.transformation import XAIBatchesError
|
|||
from litellm.types.utils import LiteLLMBatch
|
||||
|
||||
API_BASE: Final = "https://api.x.ai"
|
||||
KEY: Final = "xai-test-key"
|
||||
KEY: Final = "fake-key"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
|||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
API_BASE: Final = "https://api.x.ai"
|
||||
KEY: Final = "xai-test-key"
|
||||
KEY: Final = "fake-key"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ def test_completion_strips_message_names_from_request(respx_mock: respx.MockRout
|
|||
{"role": "assistant", "content": "OK", "name": "responder"},
|
||||
{"role": "user", "content": "Again", "name": "caller"},
|
||||
],
|
||||
api_key="xai-test-key",
|
||||
api_key="fake-key",
|
||||
api_base=XAI_API_BASE,
|
||||
)
|
||||
|
||||
|
|
@ -274,7 +274,7 @@ def test_streaming_include_usage_surfaces_xai_final_usage_chunk(
|
|||
messages=[{"role": "user", "content": "Say OK"}],
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
api_key="xai-test-key",
|
||||
api_key="fake-key",
|
||||
api_base=XAI_API_BASE,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ def _default_xai_api_base(monkeypatch: pytest.MonkeyPatch):
|
|||
def _get_models(payload: object) -> list[str]:
|
||||
with respx.mock:
|
||||
respx.get(MODELS_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
return XAIModelInfo().get_models(api_key="xai-test-key")
|
||||
return XAIModelInfo().get_models(api_key="fake-key")
|
||||
|
||||
|
||||
def test_get_models_prefixes_every_listed_model_id_in_provider_order():
|
||||
|
|
@ -47,9 +47,9 @@ def test_get_models_returns_no_names_for_an_empty_data_container(data: object):
|
|||
def test_get_models_sends_the_api_key_as_bearer_token():
|
||||
with respx.mock:
|
||||
route = respx.get(MODELS_URL).mock(return_value=httpx.Response(200, json={"data": [{"id": "grok-4"}]}))
|
||||
XAIModelInfo().get_models(api_key="xai-test-key")
|
||||
XAIModelInfo().get_models(api_key="fake-key")
|
||||
|
||||
assert route.calls[0].request.headers["authorization"] == "Bearer xai-test-key"
|
||||
assert route.calls[0].request.headers["authorization"] == "Bearer fake-key"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -101,7 +101,7 @@ def test_get_models_reports_the_upstream_status_and_body_on_an_http_error():
|
|||
with pytest.raises(
|
||||
Exception, match=re.escape("Failed to fetch models from XAI. Status code: 401, Response: bad key")
|
||||
):
|
||||
XAIModelInfo().get_models(api_key="xai-test-key")
|
||||
XAIModelInfo().get_models(api_key="fake-key")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import base64
|
||||
from io import BytesIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
|
|
@ -184,3 +185,135 @@ def test_transform_multiple_images_uses_images_array():
|
|||
{"url": "https://imgen.x.ai/a.jpeg"},
|
||||
{"url": "https://imgen.x.ai/b.jpeg"},
|
||||
]
|
||||
|
||||
|
||||
def _edit_request(image, optional_params=None):
|
||||
from litellm.llms.xai.image_edit.transformation import XAIImageEditConfig
|
||||
|
||||
data, _ = XAIImageEditConfig().transform_image_edit_request(
|
||||
model="grok-imagine-image",
|
||||
prompt="make it night",
|
||||
image=image,
|
||||
image_edit_optional_request_params=optional_params or {},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def test_validate_environment_api_key_sets_bearer_and_keeps_caller_content_type():
|
||||
from litellm.llms.xai.image_edit.transformation import XAIImageEditConfig
|
||||
|
||||
headers = XAIImageEditConfig().validate_environment(
|
||||
headers={"Content-Type": "application/custom"},
|
||||
model="grok-imagine-image",
|
||||
api_key="fake-key",
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert headers == {"Content-Type": "application/custom", "Authorization": "Bearer fake-key"}
|
||||
|
||||
|
||||
def test_map_rejects_unknown_param_unless_dropped():
|
||||
from litellm.llms.xai.image_edit.transformation import XAIImageEditConfig
|
||||
|
||||
config = XAIImageEditConfig()
|
||||
with pytest.raises(ValueError, match="Parameter quality is not supported"):
|
||||
config.map_openai_params(
|
||||
image_edit_optional_params={"quality": "hd", "n": 2},
|
||||
model="grok-imagine-image",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert config.map_openai_params(
|
||||
image_edit_optional_params={"quality": "hd", "n": 2, "resolution": "2k"},
|
||||
model="grok-imagine-image",
|
||||
drop_params=True,
|
||||
) == {"n": 2, "resolution": "2k"}
|
||||
|
||||
|
||||
def test_get_complete_url_appends_v1_to_a_bare_custom_base():
|
||||
from litellm.llms.xai.image_edit.transformation import XAIImageEditConfig
|
||||
|
||||
url = XAIImageEditConfig().get_complete_url(
|
||||
model="grok-imagine-image",
|
||||
api_base="https://xai-gateway.example.com/",
|
||||
litellm_params={"api_key": "fake-key"},
|
||||
)
|
||||
|
||||
assert url == "https://xai-gateway.example.com/v1/images/edits"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("image", [None, [], [None]])
|
||||
def test_transform_rejects_missing_reference_image(image):
|
||||
with pytest.raises(ValueError, match="requires at least one reference image"):
|
||||
_edit_request(image)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"image,expected",
|
||||
[
|
||||
({"url": "https://imgen.x.ai/a.jpeg"}, {"url": "https://imgen.x.ai/a.jpeg"}),
|
||||
({"file_id": "file-123"}, {"file_id": "file-123"}),
|
||||
],
|
||||
)
|
||||
def test_transform_passes_url_and_file_id_references_through(image, expected):
|
||||
assert _edit_request(image)["image"] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"image",
|
||||
[
|
||||
bytearray(b"\x89PNG\r\n\x1a\nfakepng"),
|
||||
BytesIO(b"\x89PNG\r\n\x1a\nfakepng"),
|
||||
],
|
||||
)
|
||||
def test_transform_encodes_binary_inputs_as_data_uri(image):
|
||||
expected = "data:image/png;base64," + base64.b64encode(b"\x89PNG\r\n\x1a\nfakepng").decode()
|
||||
|
||||
assert _edit_request(image)["image"]["url"] == expected
|
||||
|
||||
|
||||
def test_transform_reads_a_partially_consumed_stream_from_the_start_and_restores_position():
|
||||
stream = BytesIO(b"\x89PNG\r\n\x1a\nfakepng")
|
||||
stream.seek(4)
|
||||
|
||||
url = _edit_request(stream)["image"]["url"]
|
||||
|
||||
assert url.endswith(base64.b64encode(b"\x89PNG\r\n\x1a\nfakepng").decode())
|
||||
assert stream.tell() == 4
|
||||
|
||||
|
||||
def test_transform_encodes_text_reader_as_utf8():
|
||||
class TextReader:
|
||||
def read(self) -> str:
|
||||
return "plain-text-bytes"
|
||||
|
||||
url = _edit_request(TextReader())["image"]["url"]
|
||||
|
||||
assert url.endswith(base64.b64encode(b"plain-text-bytes").decode())
|
||||
|
||||
|
||||
def test_transform_rejects_unsupported_image_type():
|
||||
with pytest.raises(ValueError, match="Unsupported image input type"):
|
||||
_edit_request(12345)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw",
|
||||
[
|
||||
httpx.Response(502, text="<html>bad gateway</html>"),
|
||||
httpx.Response(200, json={"data": []}),
|
||||
],
|
||||
)
|
||||
def test_transform_response_raises_provider_error_without_images(raw):
|
||||
from litellm.llms.xai.image_edit.transformation import XAIImageEditConfig
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
XAIImageEditConfig().transform_image_edit_response(
|
||||
model="grok-imagine-image",
|
||||
raw_response=raw,
|
||||
logging_obj=MagicMock(),
|
||||
)
|
||||
|
||||
assert getattr(exc_info.value, "status_code", None) == raw.status_code
|
||||
|
|
|
|||
|
|
@ -127,3 +127,69 @@ def test_transform_request_and_response():
|
|||
)
|
||||
assert response.data is not None
|
||||
assert response.data[0].url == "https://imgen.x.ai/example.jpeg"
|
||||
|
||||
|
||||
def test_validate_environment_api_key_sets_bearer_and_json_content_type():
|
||||
headers = XAIImageGenerationConfig().validate_environment(
|
||||
headers={},
|
||||
model="grok-imagine-image",
|
||||
messages=[],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key="fake-key",
|
||||
)
|
||||
|
||||
assert headers == {"Authorization": "Bearer fake-key", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
def test_map_rejects_unknown_param_unless_dropped():
|
||||
config = XAIImageGenerationConfig()
|
||||
with pytest.raises(ValueError, match="Parameter quality is not supported"):
|
||||
config.map_openai_params(
|
||||
non_default_params={"quality": "hd", "n": 2},
|
||||
optional_params={},
|
||||
model="grok-imagine-image",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert config.map_openai_params(
|
||||
non_default_params={"quality": "hd", "n": 2},
|
||||
optional_params={},
|
||||
model="grok-imagine-image",
|
||||
drop_params=True,
|
||||
) == {"n": 2}
|
||||
|
||||
|
||||
def test_get_complete_url_appends_v1_to_a_bare_custom_base():
|
||||
url = XAIImageGenerationConfig().get_complete_url(
|
||||
api_base="https://xai-gateway.example.com/",
|
||||
api_key="fake-key",
|
||||
model="grok-imagine-image",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert url == "https://xai-gateway.example.com/v1/images/generations"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw",
|
||||
[
|
||||
httpx.Response(502, text="<html>bad gateway</html>"),
|
||||
httpx.Response(200, json={"data": ["junk"]}),
|
||||
],
|
||||
)
|
||||
def test_transform_response_raises_provider_error_without_images(raw):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
XAIImageGenerationConfig().transform_image_generation_response(
|
||||
model="grok-imagine-image",
|
||||
raw_response=raw,
|
||||
model_response=ImageResponse(),
|
||||
logging_obj=MagicMock(),
|
||||
request_data={"prompt": "a red apple"},
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
encoding=None,
|
||||
)
|
||||
|
||||
assert getattr(exc_info.value, "status_code", None) == raw.status_code
|
||||
|
|
|
|||
|
|
@ -40,6 +40,46 @@ def test_map_seconds_nine_is_not_clamped_to_six():
|
|||
assert "seconds" not in mapped
|
||||
|
||||
|
||||
def test_map_non_numeric_seconds_falls_back_to_the_default_duration():
|
||||
config = XAIVideoConfig()
|
||||
defaulted = config.map_openai_params(
|
||||
video_create_optional_params={"seconds": "ten"},
|
||||
model="grok-imagine-video-1.5",
|
||||
drop_params=True,
|
||||
)
|
||||
request_body, _, _ = config.transform_video_create_request(
|
||||
model="grok-imagine-video-1.5",
|
||||
prompt="a cat walking",
|
||||
api_base="https://api.x.ai/v1/videos/generations",
|
||||
video_create_optional_request_params={},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert defaulted["duration"] == request_body["duration"]
|
||||
|
||||
|
||||
def test_get_complete_url_appends_v1_to_a_bare_custom_base():
|
||||
url = XAIVideoConfig().get_complete_url(
|
||||
model="grok-imagine-video",
|
||||
api_base="https://xai-gateway.example.com/",
|
||||
litellm_params={"api_key": "fake-key"},
|
||||
)
|
||||
|
||||
assert url == "https://xai-gateway.example.com/v1/videos/generations"
|
||||
|
||||
|
||||
def test_create_response_without_request_id_raises():
|
||||
with pytest.raises(ValueError, match="missing request_id"):
|
||||
XAIVideoConfig().transform_video_create_response(
|
||||
model="grok-imagine-video",
|
||||
raw_response=httpx.Response(200, json={"error": "queued nowhere"}),
|
||||
logging_obj=MagicMock(),
|
||||
custom_llm_provider="xai",
|
||||
request_data={"duration": 6},
|
||||
)
|
||||
|
||||
|
||||
def test_get_complete_url_create_and_status_root():
|
||||
config = XAIVideoConfig()
|
||||
create_url = config.get_complete_url(
|
||||
|
|
@ -120,7 +160,109 @@ def test_transform_create_and_status_response():
|
|||
)
|
||||
assert status.status == "completed"
|
||||
assert status.seconds == "6"
|
||||
assert status._hidden_params.get("video_url") == "https://vidgen.x.ai/x.mp4"
|
||||
assert status.hidden_params.get("video_url") == "https://vidgen.x.ai/x.mp4"
|
||||
|
||||
|
||||
def _created_video_cost(optional_params: dict[str, object], model: str = "grok-imagine-video-1.5") -> tuple[float, str]:
|
||||
config = XAIVideoConfig()
|
||||
request_body, _, _ = config.transform_video_create_request(
|
||||
model=model,
|
||||
prompt="a cat walking",
|
||||
api_base="https://api.x.ai/v1/videos/generations",
|
||||
video_create_optional_request_params=optional_params,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
created = config.transform_video_create_response(
|
||||
model=model,
|
||||
raw_response=httpx.Response(200, json={"request_id": "req-123"}),
|
||||
logging_obj=MagicMock(),
|
||||
custom_llm_provider="xai",
|
||||
request_data=request_body,
|
||||
)
|
||||
cost = litellm.completion_cost(
|
||||
completion_response=created,
|
||||
model=f"xai/{model}",
|
||||
custom_llm_provider="xai",
|
||||
call_type="create_video",
|
||||
)
|
||||
return cost, str(created.seconds)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"optional_params,expected_seconds,rate_key",
|
||||
[
|
||||
({"duration": 10}, 10, "output_cost_per_second"),
|
||||
({"duration": "4", "resolution": "720p"}, 4, "output_cost_per_second_720p"),
|
||||
],
|
||||
)
|
||||
def test_create_response_bills_the_submitted_duration_and_resolution(optional_params, expected_seconds, rate_key):
|
||||
model_cost = litellm.model_cost["xai/grok-imagine-video-1.5"]
|
||||
assert model_cost["output_cost_per_second_720p"] != model_cost["output_cost_per_second"]
|
||||
|
||||
cost, seconds = _created_video_cost(optional_params)
|
||||
|
||||
assert seconds == str(expected_seconds)
|
||||
assert cost > 0
|
||||
assert cost == pytest.approx(expected_seconds * model_cost[rate_key])
|
||||
|
||||
|
||||
def test_create_response_bills_the_default_duration_sent_to_xai():
|
||||
config = XAIVideoConfig()
|
||||
request_body, _, _ = config.transform_video_create_request(
|
||||
model="grok-imagine-video-1.5",
|
||||
prompt="a cat walking",
|
||||
api_base="https://api.x.ai/v1/videos/generations",
|
||||
video_create_optional_request_params={},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
cost, seconds = _created_video_cost({})
|
||||
|
||||
assert seconds == str(request_body["duration"])
|
||||
assert cost == pytest.approx(
|
||||
request_body["duration"] * litellm.model_cost["xai/grok-imagine-video-1.5"]["output_cost_per_second"]
|
||||
)
|
||||
|
||||
|
||||
def test_create_request_forwards_resolution_to_xai():
|
||||
request_body, _, _ = XAIVideoConfig().transform_video_create_request(
|
||||
model="grok-imagine-video-1.5",
|
||||
prompt="a cat walking",
|
||||
api_base="https://api.x.ai/v1/videos/generations",
|
||||
video_create_optional_request_params={"resolution": "1080p"},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert request_body["resolution"] == "1080p"
|
||||
|
||||
|
||||
def test_create_response_keeps_provider_reported_usage_over_the_request():
|
||||
created = XAIVideoConfig().transform_video_create_response(
|
||||
model="grok-imagine-video-1.5",
|
||||
raw_response=httpx.Response(200, json={"request_id": "req-123", "usage": {"duration_seconds": 3.0}}),
|
||||
logging_obj=MagicMock(),
|
||||
custom_llm_provider="xai",
|
||||
request_data={"duration": 10, "resolution": "720p"},
|
||||
)
|
||||
|
||||
assert created.usage == {"duration_seconds": 3.0, "video_resolution": "720p"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("duration", [None, 0, -5, True, "abc", "nan"])
|
||||
def test_create_response_omits_unusable_durations_from_usage(duration):
|
||||
created = XAIVideoConfig().transform_video_create_response(
|
||||
model="grok-imagine-video-1.5",
|
||||
raw_response=httpx.Response(200, json={"request_id": "req-123"}),
|
||||
logging_obj=MagicMock(),
|
||||
custom_llm_provider="xai",
|
||||
request_data={"duration": duration},
|
||||
)
|
||||
|
||||
assert created.usage == {}
|
||||
assert created.seconds is None
|
||||
|
||||
|
||||
def test_sync_video_status_through_the_http_handler():
|
||||
|
|
@ -131,9 +273,7 @@ def test_sync_video_status_through_the_http_handler():
|
|||
request=httpx.Request("GET", "https://api.x.ai/v1/videos/req-123"),
|
||||
)
|
||||
|
||||
status = litellm.video_status(
|
||||
video_id="req-123", model="xai/grok-imagine-video", api_key="test-key", client=client
|
||||
)
|
||||
status = litellm.video_status(video_id="req-123", model="xai/grok-imagine-video", api_key="test-key", client=client)
|
||||
|
||||
assert status.status == "completed"
|
||||
assert client.get.call_args.kwargs["url"] == "https://api.x.ai/v1/videos/req-123"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth, hash_token
|
|||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
||||
from litellm.proxy.video_endpoints import endpoints
|
||||
from litellm.types.videos.main import VideoObject
|
||||
from litellm.types.videos.utils import encode_video_id_with_provider
|
||||
|
||||
ALICE = UserAPIKeyAuth(user_id="alice", api_key=hash_token("sk-alice"))
|
||||
|
|
@ -70,11 +71,13 @@ class Provider:
|
|||
|
||||
def __init__(self):
|
||||
self.calls: list[str] = []
|
||||
self.returns_video_objects = False
|
||||
|
||||
async def respond(self, processor, *, route_type: str, **kwargs):
|
||||
self.calls.append(route_type)
|
||||
if route_type in ("avideo_generation", "avideo_remix", "avideo_edit", "avideo_extension"):
|
||||
return {"id": f"video_{uuid.uuid4().hex}", "object": "video", "status": "queued"}
|
||||
created = {"id": f"video_{uuid.uuid4().hex}", "object": "video", "status": "queued"}
|
||||
return VideoObject(**created) if self.returns_video_objects else created
|
||||
if route_type == "avideo_content":
|
||||
return b"mp4-bytes"
|
||||
if route_type == "avideo_list":
|
||||
|
|
@ -132,11 +135,22 @@ ALL_OK = {"status": 200, "content": 200, "remix": 200, "edit": 200, "extension":
|
|||
ALL_FORBIDDEN = {"status": 403, "content": 403, "remix": 403, "edit": 403, "extension": 403}
|
||||
|
||||
|
||||
def test_owner_can_use_every_video_route(provider, table):
|
||||
@pytest.mark.parametrize("returns_video_objects", [False, True])
|
||||
def test_owner_can_use_every_video_route(provider, table, returns_video_objects):
|
||||
provider.returns_video_objects = returns_video_objects
|
||||
video_id = _create_video(ALICE)
|
||||
|
||||
assert table.rows[f"video:{video_id}"].created_by == "alice"
|
||||
assert _access(ALICE, video_id) == ALL_OK
|
||||
assert _access(BOB, video_id) == ALL_FORBIDDEN
|
||||
|
||||
|
||||
def test_caller_without_identity_scope_leaves_the_video_admin_only(provider, table):
|
||||
video_id = _create_video(UserAPIKeyAuth())
|
||||
|
||||
assert table.rows == {}
|
||||
assert _access(ALICE, video_id) == ALL_FORBIDDEN
|
||||
assert _access(ADMIN, video_id) == ALL_OK
|
||||
|
||||
|
||||
def test_other_user_is_forbidden_and_the_provider_is_never_called(provider, table):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue