Skip to content

[Feat] Add create character endpoints and other new videos Endpoints#23737

Merged
Sameerlite merged 18 commits intomainfrom
litellm_create-character-endpoint-fixes
Mar 16, 2026
Merged

[Feat] Add create character endpoints and other new videos Endpoints#23737
Sameerlite merged 18 commits intomainfrom
litellm_create-character-endpoint-fixes

Conversation

@Sameerlite
Copy link
Contributor

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test

Changes

Support missing base64 padding in managed character/video IDs so copied encoded IDs still decode to the original upstream character ID.

Made-with: Cursor
Use typed character response models and video multipart helpers so /videos/characters forwards uploaded MP4 files with video/* content type.

Made-with: Cursor
Avoid the temporary Any alias and use a concrete FileTypes import compatible with type checks.

Made-with: Cursor
@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 16, 2026 2:20pm

Request Review

@codspeed-hq
Copy link
Contributor

codspeed-hq bot commented Mar 16, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing litellm_create-character-endpoint-fixes (1a6eb01) with main (b796ee9)

Open in CodSpeed

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR adds four new video-related endpoints to LiteLLM — create character, get character, edit video, and extend video — wiring them end-to-end from the FastAPI proxy layer through the router, the BaseLLMHTTPHandler, and the provider transformation layer (OpenAI implemented; Gemini, Vertex AI, and RunwayML receive NotImplementedError stubs). It also introduces character ID encoding/decoding analogous to the existing video ID mechanism, and adds a solid suite of mock-only unit tests.

Key concerns found:

  • Fragile FileTypes import (litellm/types/videos/main.py): FileTypes is now imported from openai.types.audio.transcription_create_params — an unrelated OpenAI-internal audio module — with a # type: ignore suppressing the warning. This will break at runtime on any OpenAI SDK release that reorganizes its internal paths; the original from litellm.types.utils import FileTypes import should be restored.
  • model_id=None hard-coded in edit/extension response encoding (litellm/llms/openai/videos/transformation.py): Both transform_video_edit_response and transform_video_extension_response call encode_video_id_with_provider(video_obj.id, custom_llm_provider, None). This means the returned video ID carries no deployment/model information, so subsequent operations using that ID cannot route back to the correct deployment via resolve_model_name_from_model_id.
  • video_edit and video_extension endpoints parse body with raw orjson.loads (litellm/proxy/video_endpoints/endpoints.py): Unlike video_create_character which correctly uses _read_request_body, the edit and extension endpoints bypass the proxy's body-parsing utility. Invalid JSON will produce an unhandled 500 rather than a proper 400, and the body-caching mechanism used by all other endpoints is skipped.
  • Empty video_id not validated in video_edit/video_extension: if video.id is absent or empty the empty string is silently forwarded to the provider, producing a confusing upstream error instead of an early 422.

Confidence Score: 3/5

  • Functional for basic cases but has logic gaps in multi-deployment video ID encoding and inconsistent error handling in two of the four new proxy endpoints.
  • The core feature (character creation, retrieval, video edit, video extension) is well-structured and follows existing patterns. Tests are mock-only and comprehensive. However, the model_id=None hard-coding in edit/extension response encoding is a real semantic regression for multi-deployment setups, the raw orjson.loads in two endpoints bypasses established error-handling infrastructure, and the FileTypes import from an unrelated OpenAI-internal audio module introduces a fragile runtime dependency. These issues should be addressed before merge.
  • litellm/llms/openai/videos/transformation.py (model_id encoding), litellm/proxy/video_endpoints/endpoints.py (body parsing + empty video_id), litellm/types/videos/main.py (FileTypes import)

Important Files Changed

Filename Overview
litellm/llms/openai/videos/transformation.py Implements all 8 new transform methods for OpenAI; good character-ID decoding and MIME-type detection. Critical issue: model_id is hard-coded as None when encoding the returned video ID in edit/extension responses, breaking multi-deployment router resolution for follow-up requests.
litellm/llms/custom_httpx/llm_http_handler.py Adds sync and async handlers for character create/get, video edit, and video extension; all four handler pairs correctly call raise_for_status() before parsing the response, consistent with existing error handling patterns.
litellm/proxy/video_endpoints/endpoints.py Adds 4 new proxy endpoints; video_create_character uses _read_request_body correctly, but video_edit and video_extension use raw orjson.loads without error handling (will 500 on invalid JSON instead of 400), and both silently accept empty video_id strings.
litellm/types/videos/main.py Adds CharacterObject, VideoEditRequestParams, VideoExtensionRequestParams; fragile FileTypes import changed to openai.types.audio.transcription_create_params (unrelated OpenAI internal module) with a # type: ignore, risking ImportError on OpenAI package updates.
litellm/types/videos/utils.py Adds encode/decode helpers for character IDs mirroring the video ID pattern; also fixes a pre-existing base64 padding issue in decode_video_id_with_provider. The replace-based prefix stripping is safe here since standard base64 cannot contain underscores.
litellm/videos/main.py Adds avideo_create_character, video_create_character, avideo_get_character, video_get_character, avideo_edit, video_edit, avideo_extension, video_extension; pattern is consistent with existing video functions. Provider is correctly auto-detected from encoded IDs in edit/extension.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ProxyEndpoint as Proxy Endpoint<br/>(endpoints.py)
    participant Router as LLM Router<br/>(router.py)
    participant LiteLLMSDK as LiteLLM SDK<br/>(videos/main.py)
    participant HTTPHandler as BaseLLMHTTPHandler<br/>(llm_http_handler.py)
    participant ProviderConfig as OpenAIVideoConfig<br/>(transformation.py)
    participant Provider as OpenAI API

    Note over Client,Provider: Create Character Flow
    Client->>ProxyEndpoint: POST /v1/videos/characters (multipart: video + name)
    ProxyEndpoint->>ProxyEndpoint: _read_request_body → data{name, video, custom_llm_provider}
    ProxyEndpoint->>Router: avideo_create_character(**data)
    Router->>LiteLLMSDK: video_create_character(name, video, ...)
    LiteLLMSDK->>HTTPHandler: video_create_character_handler(...)
    HTTPHandler->>ProviderConfig: transform_video_create_character_request(name, video, ...)
    ProviderConfig-->>HTTPHandler: (url, files_list)
    HTTPHandler->>Provider: POST /videos/characters (multipart)
    Provider-->>HTTPHandler: { id, object, created_at, name }
    HTTPHandler->>ProviderConfig: transform_video_create_character_response(...)
    ProviderConfig-->>HTTPHandler: CharacterObject
    HTTPHandler-->>ProxyEndpoint: CharacterObject
    ProxyEndpoint->>ProxyEndpoint: encode_character_id_in_response() if target_model_names
    ProxyEndpoint-->>Client: CharacterObject { id: "character_<base64>" }

    Note over Client,Provider: Video Edit/Extension Flow
    Client->>ProxyEndpoint: POST /v1/videos/edits { video: {id}, prompt }
    ProxyEndpoint->>ProxyEndpoint: orjson.loads(body) → data
    ProxyEndpoint->>ProxyEndpoint: decode_video_id_with_provider(video.id) → provider, model_id
    ProxyEndpoint->>Router: avideo_edit(**data)
    Router->>LiteLLMSDK: video_edit(video_id, prompt, ...)
    LiteLLMSDK->>HTTPHandler: video_edit_handler(...)
    HTTPHandler->>ProviderConfig: transform_video_edit_request(prompt, video_id, ...)
    ProviderConfig->>ProviderConfig: extract_original_video_id(video_id)
    ProviderConfig-->>HTTPHandler: (url, { prompt, video: {id: raw_id} })
    HTTPHandler->>Provider: POST /videos/edits
    Provider-->>HTTPHandler: VideoObject
    HTTPHandler->>ProviderConfig: transform_video_edit_response(...)
    ProviderConfig->>ProviderConfig: encode_video_id_with_provider(id, provider, None ⚠️)
    ProviderConfig-->>HTTPHandler: VideoObject { id: "video_<base64, no model_id>" }
    HTTPHandler-->>ProxyEndpoint: VideoObject
    ProxyEndpoint-->>Client: VideoObject
Loading

Last reviewed commit: 1a6eb01

Comment on lines 271 to 375
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, list]:
"""
Transform the video create character request into a URL and files list (multipart).

Returns:
Tuple[str, list]: (url, files_list) for the multipart POST request
"""
pass

@abstractmethod
def transform_video_create_character_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CharacterObject:
pass

@abstractmethod
def transform_video_get_character_request(
self,
character_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform the video get character request into a URL and params.

Returns:
Tuple[str, Dict]: (url, params) for the GET request
"""
pass

@abstractmethod
def transform_video_get_character_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CharacterObject:
pass

@abstractmethod
def transform_video_edit_request(
self,
prompt: str,
video_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
extra_body: Optional[Dict[str, Any]] = None,
) -> Tuple[str, Dict]:
"""
Transform the video edit request into a URL and JSON data.

Returns:
Tuple[str, Dict]: (url, data) for the POST request
"""
pass

@abstractmethod
def transform_video_edit_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
custom_llm_provider: Optional[str] = None,
) -> VideoObject:
pass

@abstractmethod
def transform_video_extension_request(
self,
prompt: str,
video_id: str,
seconds: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
extra_body: Optional[Dict[str, Any]] = None,
) -> Tuple[str, Dict]:
"""
Transform the video extension request into a URL and JSON data.

Returns:
Tuple[str, Dict]: (url, data) for the POST request
"""
pass

@abstractmethod
def transform_video_extension_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
custom_llm_provider: Optional[str] = None,
) -> VideoObject:
pass

def get_error_class(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New abstract methods break third-party provider backward compatibility

Eight new @abstractmethod entries have been added to BaseVideoConfig. Python's ABC enforcement means any third-party class that subclasses BaseVideoConfig without implementing all eight methods will now fail to instantiate at import time (raising TypeError: Can't instantiate abstract class ... with abstract method(s) ...).

The internal providers (Gemini, RunwayML, VertexAI) work around this by adding stubs that raise NotImplementedError, but external integrators who have their own BaseVideoConfig subclasses will have their production systems broken the moment they upgrade LiteLLM.

The project's custom rule explicitly warns against backward-incompatible changes. Consider using concrete (non-abstract) base methods that raise NotImplementedError instead:

def transform_video_create_character_request(self, name, video, api_base, litellm_params, headers):
    raise NotImplementedError("video create character is not supported for this provider")

def transform_video_create_character_response(self, raw_response, logging_obj):
    raise NotImplementedError("video create character is not supported for this provider")

# ... same pattern for all 8 new methods

This keeps BaseVideoConfig concrete and gives a clear runtime error only when the unsupported operation is actually called, rather than failing at instantiation time.

Comment on lines +107 to +120


class CharacterObject(BaseModel):
"""Represents a character created from a video."""

id: str
object: Literal["character"] = "character"
created_at: int
name: str
_hidden_params: Dict[str, Any] = {}

def __contains__(self, key):
return hasattr(self, key)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_hidden_params is a shared mutable class-level default

In Pydantic v2 BaseModel, attributes whose names begin with _ are not model fields — they are ordinary Python class attributes. As written, _hidden_params: Dict[str, Any] = {} is a single dict shared by all CharacterObject instances:

a = CharacterObject(id="c1", object="character", created_at=1, name="A")
b = CharacterObject(id="c2", object="character", created_at=2, name="B")
a._hidden_params["key"] = "value"
print(b._hidden_params)  # {"key": "value"} — shared reference!

This is the classic mutable class-level default bug. The correct Pydantic approach is PrivateAttr:

Suggested change
class CharacterObject(BaseModel):
"""Represents a character created from a video."""
id: str
object: Literal["character"] = "character"
created_at: int
name: str
_hidden_params: Dict[str, Any] = {}
def __contains__(self, key):
return hasattr(self, key)
_hidden_params: Dict[str, Any] = PrivateAttr(default_factory=dict)

You'll also need to add from pydantic import PrivateAttr to the imports.

Comment on lines +6175 to +6185
api_key="",
additional_args={
"complete_input_dict": {"name": name},
"api_base": url,
"headers": headers,
},
)

try:
response = sync_httpx_client.post(
url=url,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No HTTP error check before parsing response body

All four new sync/async handler pairs (video_create_character_handler, video_get_character_handler, video_edit_handler, video_extension_handler) call transform_*_response(raw_response=response, ...) immediately after the HTTP call, without first checking whether the response was successful.

Because httpx does not raise on 4xx/5xx by default, a provider error response (e.g., {"error": "unauthorized"}) will be passed directly to CharacterObject(**raw_response.json()) or VideoObject(**raw_response.json()), causing a Pydantic ValidationError rather than a meaningful HTTP error. The subsequent except Exception as e handler may not translate this into an actionable error message for callers.

Add response.raise_for_status() before the transform call in all four new handler implementations (both sync and async variants). Example for video_create_character_handler:

response = sync_httpx_client.post(
    url=url,
    headers=headers,
    files=files_list,
    timeout=timeout,
)
response.raise_for_status()  # raises httpx.HTTPStatusError on 4xx/5xx
return video_provider_config.transform_video_create_character_response(
    raw_response=response,
    logging_obj=logging_obj,
)

This same fix applies to:

  • async_video_create_character_handler
  • video_get_character_handler / async_video_get_character_handler
  • video_edit_handler / async_video_edit_handler
  • video_extension_handler / async_video_extension_handler

Comment on lines 461 to +465
"avideo_status",
"avideo_content",
"avideo_remix",
"avideo_edit",
"avideo_extension",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avideo_create_character and avideo_get_character not included in router-first routing

The block that enables router-assisted model resolution (when an encoded ID embeds a model_id) lists avideo_edit and avideo_extension but omits avideo_create_character and avideo_get_character.

For avideo_get_character, the endpoint decodes the character_id to extract model_id_from_decoded and already calls llm_router.resolve_model_name_from_model_id inline (in endpoints.py), so the model is resolved before the router dispatch. However, avideo_create_character never resolves via the router even when a target_model_names-derived model is set. If there are multiple deployments of the same model, the router should still be given the chance to pick the right one. Consistency with the other video endpoints suggests these two should also be listed here:

"avideo_edit",
"avideo_extension",
"avideo_create_character",  # add
"avideo_get_character",     # add

… issues

- Remove duplicate DecodedCharacterId TypedDict from litellm/types/videos/main.py
- Remove dead LITELLM_MANAGED_VIDEO_CHARACTER_COMPLETE_STR constant from litellm/types/utils.py
- Add FastAPI Form validation for name field in video_create_character endpoint

Made-with: Cursor
…n video handlers

Add response.raise_for_status() before transform_*_response() calls in all eight
video character/edit/extension handler methods (sync and async):

- video_create_character_handler / async_video_create_character_handler
- video_get_character_handler / async_video_get_character_handler
- video_edit_handler / async_video_edit_handler
- video_extension_handler / async_video_extension_handler

Without these checks, httpx does not raise on 4xx/5xx responses, so provider
errors (e.g., 401 Unauthorized) pass directly to Pydantic model constructors,
causing ValidationError instead of meaningful HTTP errors. The raise_for_status()
ensures the exception handler receives proper HTTPStatusError for translation into
actionable messages.

Made-with: Cursor
…r in router-first routing

Add avideo_create_character and avideo_get_character to the list of video endpoints
that use router-first routing when a model is provided (either from decoded IDs or
target_model_names).

Previously only avideo_edit and avideo_extension were in the router-first block.
This ensures both character endpoints benefit from multi-deployment load balancing
and model resolution, making them consistent with the other video operations.

This allows:
- avideo_create_character: Router picks among multiple deployments when target_model_names is set
- avideo_get_character: Router assists with multi-model environments for consistency

Made-with: Cursor
- Clear examples for SDK and proxy usage
- Feature highlights: router support, encoding, error handling
- Best practices for character uploads and prompting
- Available from LiteLLM v1.83.0+
- Troubleshooting guide for common issues

Made-with: Cursor
- Add curl examples for avideo_edit and avideo_extension APIs
- Explain how LiteLLM encodes/decodes managed character IDs
- Show metadata included in character IDs (provider, model_id)
- Detail transparent router-first routing benefits

Made-with: Cursor
…tion test

Add avideo_create_character, avideo_get_character, avideo_edit, and avideo_extension
to the skip condition since Azure video calls don't use initialize_azure_sdk_client.

Tests now properly skip with expected behavior instead of failing:
- test_ensure_initialize_azure_sdk_client_always_used[avideo_create_character] ✓
- test_ensure_initialize_azure_sdk_client_always_used[avideo_get_character] ✓
- test_ensure_initialize_azure_sdk_client_always_used[avideo_edit] ✓
- test_ensure_initialize_azure_sdk_client_always_used[avideo_extension] ✓

Made-with: Cursor
@@ -1,10 +1,9 @@
from typing import Any, Dict, List, Literal, Optional

from openai.types.audio.transcription_create_params import FileTypes # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileTypes imported from unrelated OpenAI audio module

FileTypes is now imported from openai.types.audio.transcription_create_params, which is an OpenAI-internal module intended for audio transcription — entirely unrelated to video. The original import was from litellm.types.utils import FileTypes. The # type: ignore comment is a signal that this import is already fragile.

OpenAI has historically reorganized internal module paths between minor versions. If that happens here, this import will break at runtime with an ImportError. Prefer the stable litellm-internal source:

Suggested change
from openai.types.audio.transcription_create_params import FileTypes # type: ignore
from litellm.types.utils import FileTypes # type: ignore

Comment on lines 268 to 375
) -> VideoObject:
pass

@abstractmethod
def transform_video_create_character_request(
self,
name: str,
video: Any,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, list]:
"""
Transform the video create character request into a URL and files list (multipart).

Returns:
Tuple[str, list]: (url, files_list) for the multipart POST request
"""
pass

@abstractmethod
def transform_video_create_character_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CharacterObject:
pass

@abstractmethod
def transform_video_get_character_request(
self,
character_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform the video get character request into a URL and params.

Returns:
Tuple[str, Dict]: (url, params) for the GET request
"""
pass

@abstractmethod
def transform_video_get_character_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CharacterObject:
pass

@abstractmethod
def transform_video_edit_request(
self,
prompt: str,
video_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
extra_body: Optional[Dict[str, Any]] = None,
) -> Tuple[str, Dict]:
"""
Transform the video edit request into a URL and JSON data.

Returns:
Tuple[str, Dict]: (url, data) for the POST request
"""
pass

@abstractmethod
def transform_video_edit_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
custom_llm_provider: Optional[str] = None,
) -> VideoObject:
pass

@abstractmethod
def transform_video_extension_request(
self,
prompt: str,
video_id: str,
seconds: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
extra_body: Optional[Dict[str, Any]] = None,
) -> Tuple[str, Dict]:
"""
Transform the video extension request into a URL and JSON data.

Returns:
Tuple[str, Dict]: (url, data) for the POST request
"""
pass

@abstractmethod
def transform_video_extension_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
custom_llm_provider: Optional[str] = None,
) -> VideoObject:
pass

def get_error_class(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New abstract methods break third-party BaseVideoConfig subclasses

Eight new @abstractmethod entries have been added to BaseVideoConfig. Any third-party class that subclasses BaseVideoConfig without implementing all eight methods will now fail to instantiate at import time with a TypeError: Can't instantiate abstract class. The internal providers (Gemini, RunwayML, VertexAI) work around this by adding raise NotImplementedError(...) stubs, but external integrators will silently break the moment they upgrade LiteLLM.

This violates the project's backward-compatibility rule. Use concrete (non-abstract) methods that raise NotImplementedError instead:

def transform_video_create_character_request(self, name, video, api_base, litellm_params, headers):
    raise NotImplementedError("video create character is not supported for this provider")

Apply the same pattern to all eight new methods.

Rule Used: What: avoid backwards-incompatible changes without... (source)

…sion methods

Convert all 8 new video methods from @AbstractMethod to concrete implementations
that raise NotImplementedError. This prevents breaking external third-party
BaseVideoConfig subclasses at import time.

Methods affected:
- transform_video_create_character_request/response
- transform_video_get_character_request/response
- transform_video_edit_request/response
- transform_video_extension_request/response

External integrators can now upgrade without instantiation errors; NotImplementedError
is only raised when operations are actually called on unsupported providers.

This restores backward compatibility with the project's policy.

Made-with: Cursor
@Sameerlite Sameerlite merged commit 71dfd01 into main Mar 16, 2026
67 of 97 checks passed
Comment on lines +502 to +511
raw_response: httpx.Response,
logging_obj: Any,
) -> CharacterObject:
return CharacterObject(**raw_response.json())

def transform_video_edit_request(
self,
prompt: str,
video_id: str,
api_base: str,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model_id always None in edit/extension response video ID

transform_video_edit_response and transform_video_extension_response both call encode_video_id_with_provider(video_obj.id, custom_llm_provider, None), hard-coding model_id=None. This means the encoded video ID returned to the caller will not carry any model/deployment information.

For multi-deployment scenarios, a client that later uses this returned video ID for a follow-up operation (another edit, an extension, or a status check) will not be able to route back to the same deployment via resolve_model_name_from_model_id, since the model ID was never stored. The litellm_params object is available in the HTTP handler and contains the deployment's model_id — consider threading it into the transform methods and using it here:

def transform_video_edit_response(
    self,
    raw_response: httpx.Response,
    logging_obj: Any,
    custom_llm_provider: Optional[str] = None,
    model_id: Optional[str] = None,  # add parameter
) -> VideoObject:
    video_obj = VideoObject(**raw_response.json())
    if custom_llm_provider and video_obj.id:
        video_obj.id = encode_video_id_with_provider(
            video_obj.id, custom_llm_provider, model_id  # use model_id
        )
    return video_obj

Apply the same fix to transform_video_extension_response.

Comment on lines +700 to +705
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
route_type="avideo_get_character",
proxy_logging_obj=proxy_logging_obj,
llm_router=llm_router,
general_settings=general_settings,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw orjson.loads lacks error handling for invalid JSON

video_edit and video_extension parse the request body directly with orjson.loads(body), while the video_create_character endpoint in the same file uses _read_request_body(request=request). The _read_request_body utility includes:

  • Proper HTTP 400 error responses via ProxyException on invalid JSON
  • Body caching to prevent double-reads
  • Fallback json.loads with surrogate-pair cleanup

With raw orjson.loads, a malformed request body will raise an unhandled orjson.JSONDecodeError that surfaces as a 500 error to the caller instead of a proper 400. It also means these endpoints miss the body-caching mechanism used by every other proxy endpoint.

Replace both body-parsing blocks with _read_request_body:

data = await _read_request_body(request=request)

Then keep the video_ref = data.pop("video", {}) extraction unchanged below it.

Comment on lines +706 to +709
proxy_config=proxy_config,
select_data_generator=select_data_generator,
model=None,
user_model=user_model,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent empty video_id when video.id is absent

If the caller omits the id field inside the video object, or sends "video": {}, video_id is silently set to an empty string "":

video_ref = data.pop("video", {})
video_id = video_ref.get("id", "") if isinstance(video_ref, dict) else ""
data["video_id"] = video_id

This empty string is forwarded all the way to the provider without any validation, which will produce a confusing provider error rather than a clear 422. The same pattern is repeated in video_extension. Consider adding an early check:

Suggested change
proxy_config=proxy_config,
select_data_generator=select_data_generator,
model=None,
user_model=user_model,
video_ref = data.pop("video", {})
video_id = video_ref.get("id", "") if isinstance(video_ref, dict) else ""
if not video_id:
raise ProxyException(
message="'video.id' is required for video edits",
type="invalid_request_error",
param="video.id",
code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
data["video_id"] = video_id

@@ -1,10 +1,9 @@
from typing import Any, Dict, List, Literal, Optional

from openai.types.audio.transcription_create_params import FileTypes # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile FileTypes import from unrelated OpenAI audio module

FileTypes is now imported from openai.types.audio.transcription_create_params — an internal module for audio transcription that is unrelated to video. The original import was from litellm.types.utils import FileTypes. The # type: ignore comment is already a signal that this path is fragile.

OpenAI has restructured internal module paths between minor versions before. If that happens here this import will break at runtime with an ImportError that has no graceful fallback.

Revert to the stable internal source:

Suggested change
from openai.types.audio.transcription_create_params import FileTypes # type: ignore
from litellm.types.utils import FileTypes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants