gproxy_protocol/protocol/openai/video/
mod.rs1mod 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
24pub 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}