Skip to main content

gproxy_protocol/protocol/openai/video/
mod.rs

1mod enums;
2mod requests;
3mod responses;
4
5pub use enums::*;
6pub use requests::*;
7pub use responses::*;
8
9use super::common::OpenAiWireModel;
10use crate::protocol::{Endpoint, HttpMethod, Operation, Provider};
11
12pub type CreateVideoWireModel = OpenAiWireModel<CreateVideoRequest, Video>;
13pub type RetrieveVideoWireModel = OpenAiWireModel<RetrieveVideoRequest, Video>;
14pub type ListVideosWireModel = OpenAiWireModel<ListVideosRequest, VideoListResponse>;
15pub type DeleteVideoWireModel = OpenAiWireModel<DeleteVideoRequest, VideoDeleteResponse>;
16pub type DownloadVideoContentWireModel = OpenAiWireModel<DownloadVideoContentRequest, Vec<u8>>;
17pub type RemixVideoWireModel = OpenAiWireModel<RemixVideoRequest, Video>;
18pub type CreateVideoCharacterWireModel =
19    OpenAiWireModel<CreateVideoCharacterRequest, VideoCharacter>;
20pub type GetVideoCharacterWireModel = OpenAiWireModel<GetVideoCharacterRequest, VideoCharacter>;
21pub type EditVideoWireModel = OpenAiWireModel<EditVideoRequest, Video>;
22pub type ExtendVideoWireModel = OpenAiWireModel<ExtendVideoRequest, Video>;
23
24/// OpenAI video endpoint metadata. Resource identifiers remain path-template
25/// parameters so routers can bind them from the inbound request.
26pub fn openai_video_endpoints() -> [Endpoint; 10] {
27    use HttpMethod::{Delete, Get, Post};
28    use Operation::{
29        CreateVideo, CreateVideoCharacter, DeleteVideo, DownloadVideoContent, EditVideo,
30        ExtendVideo, GetVideoCharacter, ListVideos, RemixVideo, RetrieveVideo,
31    };
32
33    [
34        Endpoint::provider(CreateVideo, Provider::OpenAi, Post, "/v1/videos"),
35        Endpoint::provider(
36            RetrieveVideo,
37            Provider::OpenAi,
38            Get,
39            "/v1/videos/{video_id}",
40        ),
41        Endpoint::provider(ListVideos, Provider::OpenAi, Get, "/v1/videos"),
42        Endpoint::provider(
43            DeleteVideo,
44            Provider::OpenAi,
45            Delete,
46            "/v1/videos/{video_id}",
47        ),
48        Endpoint::provider(
49            DownloadVideoContent,
50            Provider::OpenAi,
51            Get,
52            "/v1/videos/{video_id}/content",
53        ),
54        Endpoint::provider(
55            RemixVideo,
56            Provider::OpenAi,
57            Post,
58            "/v1/videos/{video_id}/remix",
59        ),
60        Endpoint::provider(
61            CreateVideoCharacter,
62            Provider::OpenAi,
63            Post,
64            "/v1/videos/characters",
65        ),
66        Endpoint::provider(
67            GetVideoCharacter,
68            Provider::OpenAi,
69            Get,
70            "/v1/videos/characters/{character_id}",
71        ),
72        Endpoint::provider(EditVideo, Provider::OpenAi, Post, "/v1/videos/edits"),
73        Endpoint::provider(ExtendVideo, Provider::OpenAi, Post, "/v1/videos/extensions"),
74    ]
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn create_video_models_multipart_and_json_references() {
83        let file: CreateVideoRequest = serde_json::from_value(serde_json::json!({
84            "prompt": "A calico cat playing piano",
85            "input_reference": "data:image/png;base64,AAAA",
86            "model": "sora-2",
87            "seconds": "8",
88            "size": "1024x1792"
89        }))
90        .unwrap();
91        assert!(matches!(
92            file.input_reference,
93            Some(VideoInputReference::File(_))
94        ));
95
96        let object: CreateVideoRequest = serde_json::from_value(serde_json::json!({
97            "prompt": "A calico cat playing piano",
98            "input_reference": {"file_id": "file_123"}
99        }))
100        .unwrap();
101        assert!(matches!(
102            object.input_reference,
103            Some(VideoInputReference::Image(_))
104        ));
105    }
106
107    #[test]
108    fn image_reference_requires_exactly_one_source() {
109        assert!(serde_json::from_value::<VideoImageReference>(serde_json::json!({})).is_err());
110        assert!(
111            serde_json::from_value::<VideoImageReference>(serde_json::json!({
112                "file_id": "file_123",
113                "image_url": "https://example.com/reference.png"
114            }))
115            .is_err()
116        );
117    }
118
119    #[test]
120    fn video_response_preserves_future_enum_values_and_fields() {
121        let video: Video = serde_json::from_value(serde_json::json!({
122            "id": "video_123",
123            "object": "video",
124            "model": "sora-next",
125            "status": "paused",
126            "progress": 42,
127            "created_at": 1712697600,
128            "size": "2048x2048",
129            "seconds": "16",
130            "future_field": true
131        }))
132        .unwrap();
133        assert!(matches!(video.model, VideoModelId::Unknown(_)));
134        assert!(matches!(video.status, VideoStatus::Unknown(_)));
135        assert!(matches!(video.size, VideoSize::Unknown(_)));
136        assert_eq!(
137            video.extra.get("future_field"),
138            Some(&serde_json::json!(true))
139        );
140    }
141
142    #[test]
143    fn extension_accepts_documented_longer_durations() {
144        let request: ExtendVideoRequest = serde_json::from_value(serde_json::json!({
145            "prompt": "Continue the scene",
146            "seconds": "20",
147            "video": {"id": "video_123"}
148        }))
149        .unwrap();
150        assert!(matches!(
151            request.seconds,
152            VideoExtensionSeconds::Known(VideoExtensionSecondsKnown::Twenty)
153        ));
154        assert!(matches!(request.video, VideoReference::Existing(_)));
155    }
156
157    #[test]
158    fn list_and_delete_responses_model_cursor_and_discriminator() {
159        let list: VideoListResponse = serde_json::from_value(serde_json::json!({
160            "data": [],
161            "object": "list",
162            "has_more": false,
163            "last_id": null
164        }))
165        .unwrap();
166        assert_eq!(list.has_more, Some(false));
167
168        let deleted: VideoDeleteResponse = serde_json::from_value(serde_json::json!({
169            "id": "video_123",
170            "deleted": true,
171            "object": "video.deleted"
172        }))
173        .unwrap();
174        assert!(deleted.deleted);
175    }
176
177    #[test]
178    fn endpoint_metadata_covers_every_video_operation() {
179        let endpoints = openai_video_endpoints();
180        assert_eq!(endpoints.len(), 10);
181        assert!(endpoints.iter().all(|endpoint| {
182            endpoint.provider_family() == Provider::OpenAi
183                && endpoint.group() == crate::protocol::OperationGroup::Video
184        }));
185        assert!(endpoints.iter().any(|endpoint| {
186            endpoint.operation_key.operation() == Operation::DownloadVideoContent
187                && endpoint.method == HttpMethod::Get
188                && endpoint.path == "/v1/videos/{video_id}/content"
189        }));
190        assert!(endpoints.iter().any(|endpoint| {
191            endpoint.operation_key.operation() == Operation::ExtendVideo
192                && endpoint.method == HttpMethod::Post
193                && endpoint.path == "/v1/videos/extensions"
194        }));
195    }
196}