mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
15
crates/aether-video-tasks-core/Cargo.toml
Normal file
15
crates/aether-video-tasks-core/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "aether-video-tasks-core"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
aether-contracts.workspace = true
|
||||
aether-data.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
108
crates/aether-video-tasks-core/src/body.rs
Normal file
108
crates/aether-video-tasks-core/src/body.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub fn context_text(context: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
context
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub fn context_u64(context: &Map<String, Value>, key: &str) -> Option<u64> {
|
||||
let value = context.get(key)?;
|
||||
match value {
|
||||
Value::Number(number) => number.as_u64(),
|
||||
Value::String(text) => text.trim().parse().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_body_text(context: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
context
|
||||
.get("original_request_body")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|body| body.get(key))
|
||||
.and_then(|value| match value {
|
||||
Value::String(text) => Some(text.trim().to_string()),
|
||||
Value::Number(number) => Some(number.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn request_body_string(body: &Value, key: &str) -> Option<String> {
|
||||
body.as_object()
|
||||
.and_then(|map| map.get(key))
|
||||
.and_then(|value| match value {
|
||||
Value::String(text) => Some(text.trim().to_string()),
|
||||
Value::Number(number) => Some(number.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn request_body_u32(body: &Value, key: &str) -> Option<u32> {
|
||||
body.as_object()
|
||||
.and_then(|map| map.get(key))
|
||||
.and_then(|value| match value {
|
||||
Value::Number(number) => number.as_u64().and_then(|value| u32::try_from(value).ok()),
|
||||
Value::String(text) => text.trim().parse().ok(),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
context_text, context_u64, request_body_string, request_body_text, request_body_u32,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn context_helpers_trim_and_parse_values() {
|
||||
let context = json!({
|
||||
"request_id": " req_123 ",
|
||||
"local_created_at": "42",
|
||||
});
|
||||
|
||||
let context = context.as_object().expect("object");
|
||||
assert_eq!(
|
||||
context_text(context, "request_id").as_deref(),
|
||||
Some("req_123")
|
||||
);
|
||||
assert_eq!(context_u64(context, "local_created_at"), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_body_helpers_read_nested_original_request_body() {
|
||||
let context = json!({
|
||||
"original_request_body": {
|
||||
"prompt": " hello ",
|
||||
"seconds": "8",
|
||||
}
|
||||
});
|
||||
|
||||
let context = context.as_object().expect("object");
|
||||
assert_eq!(
|
||||
request_body_text(context, "prompt").as_deref(),
|
||||
Some("hello")
|
||||
);
|
||||
assert_eq!(request_body_text(context, "seconds").as_deref(), Some("8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_body_helpers_read_flat_json_values() {
|
||||
let body = json!({
|
||||
"prompt": " hello ",
|
||||
"seconds": 8,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
request_body_string(&body, "prompt").as_deref(),
|
||||
Some("hello")
|
||||
);
|
||||
assert_eq!(request_body_u32(&body, "seconds"), Some(8));
|
||||
}
|
||||
}
|
||||
123
crates/aether-video-tasks-core/src/follow_up.rs
Normal file
123
crates/aether-video-tasks-core/src/follow_up.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub fn build_video_follow_up_report_context(
|
||||
request_id: &str,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
task_id: &str,
|
||||
provider_id: &str,
|
||||
endpoint_id: &str,
|
||||
key_id: &str,
|
||||
provider_name: Option<&str>,
|
||||
model_name: Option<&str>,
|
||||
client_api_format: &str,
|
||||
provider_api_format: &str,
|
||||
) -> Value {
|
||||
let mut context = Map::new();
|
||||
context.insert(
|
||||
"request_id".to_string(),
|
||||
Value::String(request_id.to_string()),
|
||||
);
|
||||
context.insert("user_id".to_string(), Value::String(user_id.to_string()));
|
||||
context.insert(
|
||||
"api_key_id".to_string(),
|
||||
Value::String(api_key_id.to_string()),
|
||||
);
|
||||
context.insert("task_id".to_string(), Value::String(task_id.to_string()));
|
||||
context.insert(
|
||||
"provider_id".to_string(),
|
||||
Value::String(provider_id.to_string()),
|
||||
);
|
||||
context.insert(
|
||||
"endpoint_id".to_string(),
|
||||
Value::String(endpoint_id.to_string()),
|
||||
);
|
||||
context.insert("key_id".to_string(), Value::String(key_id.to_string()));
|
||||
context.insert(
|
||||
"client_api_format".to_string(),
|
||||
Value::String(client_api_format.to_string()),
|
||||
);
|
||||
context.insert(
|
||||
"provider_api_format".to_string(),
|
||||
Value::String(provider_api_format.to_string()),
|
||||
);
|
||||
if let Some(provider_name) = provider_name.filter(|value| !value.is_empty()) {
|
||||
context.insert(
|
||||
"provider_name".to_string(),
|
||||
Value::String(provider_name.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(model_name) = model_name.filter(|value| !value.is_empty()) {
|
||||
context.insert("model".to_string(), Value::String(model_name.to_string()));
|
||||
}
|
||||
Value::Object(context)
|
||||
}
|
||||
|
||||
pub fn resolve_follow_up_auth(
|
||||
user_id: Option<&str>,
|
||||
api_key_id: Option<&str>,
|
||||
fallback_user_id: Option<&str>,
|
||||
fallback_api_key_id: Option<&str>,
|
||||
) -> Option<(String, String)> {
|
||||
let resolved_user_id = user_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
fallback_user_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
})?;
|
||||
let resolved_api_key_id = api_key_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
fallback_api_key_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
})?;
|
||||
Some((resolved_user_id, resolved_api_key_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_video_follow_up_report_context, resolve_follow_up_auth};
|
||||
|
||||
#[test]
|
||||
fn builds_follow_up_report_context_with_transport_metadata() {
|
||||
let context = build_video_follow_up_report_context(
|
||||
"req_123",
|
||||
"user_123",
|
||||
"key_123",
|
||||
"task_123",
|
||||
"provider_123",
|
||||
"endpoint_123",
|
||||
"transport_key_123",
|
||||
Some("provider-name"),
|
||||
Some("model-name"),
|
||||
"openai:video",
|
||||
"openai:video",
|
||||
);
|
||||
|
||||
assert_eq!(context["request_id"].as_str(), Some("req_123"));
|
||||
assert_eq!(context["provider_id"].as_str(), Some("provider_123"));
|
||||
assert_eq!(context["provider_name"].as_str(), Some("provider-name"));
|
||||
assert_eq!(context["model"].as_str(), Some("model-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_follow_up_auth_from_primary_or_fallback_values() {
|
||||
assert_eq!(
|
||||
resolve_follow_up_auth(Some(" user_123 "), Some(" key_123 "), None, None),
|
||||
Some(("user_123".to_string(), "key_123".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_follow_up_auth(None, None, Some("user_fallback"), Some("key_fallback")),
|
||||
Some(("user_fallback".to_string(), "key_fallback".to_string()))
|
||||
);
|
||||
assert_eq!(resolve_follow_up_auth(None, None, None, None), None);
|
||||
}
|
||||
}
|
||||
419
crates/aether-video-tasks-core/src/gemini.rs
Normal file
419
crates/aether-video-tasks-core/src/gemini.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
use aether_contracts::{ExecutionPlan, RequestBody};
|
||||
use aether_data::repository::video_tasks::{StoredVideoTask, UpsertVideoTask, VideoTaskStatus};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
build_video_follow_up_report_context, current_unix_timestamp_secs, gemini_metadata_video_url,
|
||||
request_body_string, request_body_u32, resolve_follow_up_auth, GeminiVideoTaskSeed,
|
||||
LocalVideoTaskFollowUpPlan, LocalVideoTaskReadResponse, LocalVideoTaskSnapshot,
|
||||
LocalVideoTaskStatus, DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
|
||||
DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
|
||||
};
|
||||
|
||||
pub fn map_gemini_stored_task_to_read_response(
|
||||
task: StoredVideoTask,
|
||||
) -> LocalVideoTaskReadResponse {
|
||||
match task.status {
|
||||
VideoTaskStatus::Cancelled => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task was cancelled"}),
|
||||
},
|
||||
VideoTaskStatus::Deleted => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task not found"}),
|
||||
},
|
||||
VideoTaskStatus::Completed => LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: build_gemini_completed_body(task),
|
||||
},
|
||||
VideoTaskStatus::Failed | VideoTaskStatus::Expired => LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: build_gemini_failed_body(task),
|
||||
},
|
||||
_ => LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: build_gemini_pending_body(task),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn build_gemini_completed_body(task: StoredVideoTask) -> Value {
|
||||
let operation_name = stored_task_operation_name(&task);
|
||||
let short_id = task.short_id.unwrap_or_default();
|
||||
|
||||
json!({
|
||||
"name": operation_name,
|
||||
"done": true,
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": format!("/v1beta/files/aev_{short_id}:download?alt=media"),
|
||||
"mimeType": "video/mp4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gemini_failed_body(task: StoredVideoTask) -> Value {
|
||||
json!({
|
||||
"name": stored_task_operation_name(&task),
|
||||
"done": true,
|
||||
"error": {
|
||||
"code": task.error_code.unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||
"message": task
|
||||
.error_message
|
||||
.unwrap_or_else(|| "Video generation failed".to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gemini_pending_body(task: StoredVideoTask) -> Value {
|
||||
json!({
|
||||
"name": stored_task_operation_name(&task),
|
||||
"done": false,
|
||||
"metadata": {}
|
||||
})
|
||||
}
|
||||
|
||||
fn stored_task_operation_name(task: &StoredVideoTask) -> String {
|
||||
let model = task.model.clone().unwrap_or_else(|| "unknown".to_string());
|
||||
let short_id = task.short_id.clone().unwrap_or_else(|| task.id.clone());
|
||||
format!("models/{model}/operations/{short_id}")
|
||||
}
|
||||
|
||||
impl GeminiVideoTaskSeed {
|
||||
pub fn apply_provider_body(&mut self, provider_body: &Map<String, Value>) {
|
||||
let done = provider_body
|
||||
.get("done")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
if done {
|
||||
let error = provider_body.get("error").and_then(Value::as_object);
|
||||
if let Some(error) = error {
|
||||
self.status = LocalVideoTaskStatus::Failed;
|
||||
self.progress_percent = 100;
|
||||
self.error_code = error
|
||||
.get("code")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
self.error_message = error
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
} else {
|
||||
self.status = LocalVideoTaskStatus::Completed;
|
||||
self.progress_percent = 100;
|
||||
self.error_code = None;
|
||||
self.error_message = None;
|
||||
}
|
||||
self.metadata = json!({});
|
||||
return;
|
||||
}
|
||||
|
||||
self.status = LocalVideoTaskStatus::Processing;
|
||||
self.progress_percent = 50;
|
||||
self.error_code = None;
|
||||
self.error_message = None;
|
||||
self.metadata = provider_body
|
||||
.get("metadata")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
}
|
||||
|
||||
pub fn build_get_follow_up_plan(&self, trace_id: &str) -> Option<ExecutionPlan> {
|
||||
if !matches!(
|
||||
self.status,
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let operation_path = self.resolve_operation_path()?;
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-type");
|
||||
headers.remove("content-length");
|
||||
|
||||
Some(ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "GET".to_string(),
|
||||
url: format!(
|
||||
"{}/v1beta/{}",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
operation_path
|
||||
),
|
||||
headers,
|
||||
content_type: None,
|
||||
content_encoding: None,
|
||||
body: RequestBody {
|
||||
json_body: None,
|
||||
body_bytes_b64: None,
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "gemini:video".to_string(),
|
||||
provider_api_format: "gemini:video".to_string(),
|
||||
model_name: Some(self.model.clone()),
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_cancel_follow_up_plan(
|
||||
&self,
|
||||
fallback_user_id: Option<&str>,
|
||||
fallback_api_key_id: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskFollowUpPlan> {
|
||||
if !matches!(
|
||||
self.status,
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let (user_id, api_key_id) = resolve_follow_up_auth(
|
||||
self.user_id.as_deref(),
|
||||
self.api_key_id.as_deref(),
|
||||
fallback_user_id,
|
||||
fallback_api_key_id,
|
||||
)?;
|
||||
|
||||
let operation_path = self.resolve_operation_path()?;
|
||||
let mut headers = self.transport.headers.clone();
|
||||
let content_type = self
|
||||
.transport
|
||||
.content_type
|
||||
.clone()
|
||||
.unwrap_or_else(|| "application/json".to_string());
|
||||
headers
|
||||
.entry("content-type".to_string())
|
||||
.or_insert_with(|| content_type.clone());
|
||||
|
||||
Some(LocalVideoTaskFollowUpPlan {
|
||||
plan: ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "POST".to_string(),
|
||||
url: format!(
|
||||
"{}/v1beta/{}:cancel",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
operation_path
|
||||
),
|
||||
headers,
|
||||
content_type: Some(content_type),
|
||||
content_encoding: None,
|
||||
body: RequestBody::from_json(json!({})),
|
||||
stream: false,
|
||||
client_api_format: "gemini:video".to_string(),
|
||||
provider_api_format: "gemini:video".to_string(),
|
||||
model_name: Some(self.model.clone()),
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
},
|
||||
report_kind: Some("gemini_video_cancel_sync_finalize".to_string()),
|
||||
report_context: Some(build_video_follow_up_report_context(
|
||||
&self.persistence.request_id,
|
||||
&user_id,
|
||||
&api_key_id,
|
||||
&self.local_short_id,
|
||||
&self.transport.provider_id,
|
||||
&self.transport.endpoint_id,
|
||||
&self.transport.key_id,
|
||||
self.transport.provider_name.as_deref(),
|
||||
Some(self.model.as_str()),
|
||||
"gemini:video",
|
||||
"gemini:video",
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn client_body_json(&self) -> Value {
|
||||
let operation_name = format!("models/{}/operations/{}", self.model, self.local_short_id);
|
||||
match self.status {
|
||||
LocalVideoTaskStatus::Completed => json!({
|
||||
"name": operation_name,
|
||||
"done": true,
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": format!("/v1beta/files/aev_{}:download?alt=media", self.local_short_id),
|
||||
"mimeType": "video/mp4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
LocalVideoTaskStatus::Failed | LocalVideoTaskStatus::Expired => json!({
|
||||
"name": operation_name,
|
||||
"done": true,
|
||||
"error": {
|
||||
"code": self.error_code.clone().unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||
"message": self
|
||||
.error_message
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Video generation failed".to_string()),
|
||||
}
|
||||
}),
|
||||
_ => json!({
|
||||
"name": operation_name,
|
||||
"done": false,
|
||||
"metadata": self.metadata.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_upsert_record(&self) -> UpsertVideoTask {
|
||||
let now_unix_secs = current_unix_timestamp_secs();
|
||||
let next_poll_at_unix_secs = match self.status {
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing => Some(
|
||||
now_unix_secs.saturating_add(u64::from(DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS)),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
UpsertVideoTask {
|
||||
id: self.local_short_id.clone(),
|
||||
short_id: Some(self.local_short_id.clone()),
|
||||
request_id: self.persistence.request_id.clone(),
|
||||
user_id: self.user_id.clone(),
|
||||
api_key_id: self.api_key_id.clone(),
|
||||
username: self.persistence.username.clone(),
|
||||
api_key_name: self.persistence.api_key_name.clone(),
|
||||
external_task_id: Some(self.upstream_operation_name.clone()),
|
||||
provider_id: Some(self.transport.provider_id.clone()),
|
||||
endpoint_id: Some(self.transport.endpoint_id.clone()),
|
||||
key_id: Some(self.transport.key_id.clone()),
|
||||
client_api_format: Some(self.persistence.client_api_format.clone()),
|
||||
provider_api_format: Some(self.persistence.provider_api_format.clone()),
|
||||
format_converted: self.persistence.format_converted,
|
||||
model: Some(self.model.clone()),
|
||||
prompt: request_body_string(&self.persistence.original_request_body, "prompt")
|
||||
.or_else(|| Some(String::new())),
|
||||
original_request_body: Some(self.persistence.original_request_body.clone()),
|
||||
duration_seconds: request_body_u32(&self.persistence.original_request_body, "seconds")
|
||||
.or_else(|| {
|
||||
request_body_u32(&self.persistence.original_request_body, "duration_seconds")
|
||||
}),
|
||||
resolution: request_body_string(&self.persistence.original_request_body, "resolution"),
|
||||
aspect_ratio: request_body_string(
|
||||
&self.persistence.original_request_body,
|
||||
"aspect_ratio",
|
||||
),
|
||||
size: request_body_string(&self.persistence.original_request_body, "size"),
|
||||
status: self.status.as_database_status(),
|
||||
progress_percent: self.progress_percent,
|
||||
progress_message: None,
|
||||
retry_count: 0,
|
||||
poll_interval_seconds: DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
|
||||
next_poll_at_unix_secs,
|
||||
poll_count: 0,
|
||||
max_poll_count: DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
submitted_at_unix_secs: Some(now_unix_secs),
|
||||
completed_at_unix_secs: None,
|
||||
updated_at_unix_secs: now_unix_secs,
|
||||
error_code: self.error_code.clone(),
|
||||
error_message: self.error_message.clone(),
|
||||
video_url: gemini_metadata_video_url(&self.metadata),
|
||||
request_metadata: Some(json!({
|
||||
"rust_owner": "async_task",
|
||||
"rust_local_snapshot": LocalVideoTaskSnapshot::Gemini(self.clone()),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_operation_path(&self) -> Option<String> {
|
||||
if self.upstream_operation_name.starts_with("models/") {
|
||||
Some(self.upstream_operation_name.clone())
|
||||
} else if self.upstream_operation_name.starts_with("operations/") && !self.model.is_empty()
|
||||
{
|
||||
Some(format!(
|
||||
"models/{}/{}",
|
||||
self.model, self.upstream_operation_name
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aether_data::repository::video_tasks::{StoredVideoTask, VideoTaskStatus};
|
||||
|
||||
use super::map_gemini_stored_task_to_read_response;
|
||||
|
||||
fn sample_stored_task(status: VideoTaskStatus) -> StoredVideoTask {
|
||||
StoredVideoTask {
|
||||
id: "task-gemini-123".to_string(),
|
||||
short_id: Some("localshort123".to_string()),
|
||||
request_id: "req-gemini-123".to_string(),
|
||||
user_id: None,
|
||||
api_key_id: None,
|
||||
username: None,
|
||||
api_key_name: None,
|
||||
external_task_id: Some("operations/ext-gemini-123".to_string()),
|
||||
provider_id: None,
|
||||
endpoint_id: None,
|
||||
key_id: None,
|
||||
client_api_format: Some("gemini:video".to_string()),
|
||||
provider_api_format: Some("gemini:video".to_string()),
|
||||
format_converted: false,
|
||||
model: Some("veo-3".to_string()),
|
||||
prompt: None,
|
||||
original_request_body: None,
|
||||
duration_seconds: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
size: None,
|
||||
status,
|
||||
progress_percent: 50,
|
||||
progress_message: None,
|
||||
retry_count: 0,
|
||||
poll_interval_seconds: 10,
|
||||
next_poll_at_unix_secs: None,
|
||||
poll_count: 0,
|
||||
max_poll_count: 360,
|
||||
created_at_unix_secs: 1712345678,
|
||||
submitted_at_unix_secs: Some(1712345678),
|
||||
completed_at_unix_secs: None,
|
||||
updated_at_unix_secs: 1712345679,
|
||||
error_code: Some("UNKNOWN".to_string()),
|
||||
error_message: Some("provider failed".to_string()),
|
||||
video_url: None,
|
||||
request_metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_cancelled_gemini_stored_task_into_not_found_response() {
|
||||
let response =
|
||||
map_gemini_stored_task_to_read_response(sample_stored_task(VideoTaskStatus::Cancelled));
|
||||
|
||||
assert_eq!(response.status_code, 404);
|
||||
assert_eq!(response.body_json["detail"], "Video task was cancelled");
|
||||
}
|
||||
}
|
||||
55
crates/aether-video-tasks-core/src/lib.rs
Normal file
55
crates/aether-video-tasks-core/src/lib.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
mod body;
|
||||
mod follow_up;
|
||||
mod gemini;
|
||||
mod openai;
|
||||
mod path;
|
||||
mod read_side;
|
||||
mod service;
|
||||
mod snapshot;
|
||||
mod store;
|
||||
mod store_backend;
|
||||
mod store_registry;
|
||||
mod sync;
|
||||
mod transport;
|
||||
mod transport_domain;
|
||||
mod types;
|
||||
mod util;
|
||||
|
||||
pub use body::{
|
||||
context_text, context_u64, request_body_string, request_body_text, request_body_u32,
|
||||
};
|
||||
pub use follow_up::{build_video_follow_up_report_context, resolve_follow_up_auth};
|
||||
pub use gemini::map_gemini_stored_task_to_read_response;
|
||||
pub use openai::map_openai_stored_task_to_read_response;
|
||||
pub use path::{
|
||||
build_local_sync_finalize_request_path, current_unix_timestamp_secs,
|
||||
extract_gemini_short_id_from_cancel_path, extract_gemini_short_id_from_path,
|
||||
extract_openai_task_id_from_cancel_path, extract_openai_task_id_from_content_path,
|
||||
extract_openai_task_id_from_path, extract_openai_task_id_from_remix_path,
|
||||
generate_local_short_id, local_status_from_stored, resolve_local_video_registry_mutation,
|
||||
resolve_video_task_hydration_lookup_key, resolve_video_task_read_lookup_key,
|
||||
resolve_video_task_report_lookup, VideoTaskReportLookup,
|
||||
};
|
||||
pub use read_side::{read_data_backed_video_task_response, StoredVideoTaskReadSide};
|
||||
pub use service::VideoTaskService;
|
||||
pub use store::VideoTaskStore;
|
||||
pub use store_backend::{FileVideoTaskStore, InMemoryVideoTaskStore};
|
||||
pub use store_registry::VideoTaskRegistry;
|
||||
pub use sync::{
|
||||
build_internal_finalize_video_plan, build_local_sync_finalize_read_response,
|
||||
resolve_local_sync_error_background_report_kind,
|
||||
resolve_local_sync_success_background_report_kind,
|
||||
};
|
||||
pub use transport::{
|
||||
gemini_metadata_video_url, map_openai_task_status, parse_video_content_variant,
|
||||
};
|
||||
pub use types::{
|
||||
GeminiVideoTaskSeed, LocalVideoTaskContentAction, LocalVideoTaskFollowUpPlan,
|
||||
LocalVideoTaskPersistence, LocalVideoTaskProjectionTarget, LocalVideoTaskReadRefreshPlan,
|
||||
LocalVideoTaskReadResponse, LocalVideoTaskRegistryMutation, LocalVideoTaskSeed,
|
||||
LocalVideoTaskSnapshot, LocalVideoTaskStatus, LocalVideoTaskSuccessPlan,
|
||||
LocalVideoTaskTransport, LocalVideoTaskTransportBridgeInput, OpenAiVideoTaskSeed,
|
||||
VideoTaskSyncReportMode, VideoTaskTruthSourceMode, DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
|
||||
DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
|
||||
};
|
||||
pub use util::non_empty_owned;
|
||||
684
crates/aether-video-tasks-core/src/openai.rs
Normal file
684
crates/aether-video-tasks-core/src/openai.rs
Normal file
@@ -0,0 +1,684 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aether_contracts::{ExecutionPlan, RequestBody};
|
||||
use aether_data::repository::video_tasks::{StoredVideoTask, UpsertVideoTask, VideoTaskStatus};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
build_video_follow_up_report_context, current_unix_timestamp_secs, map_openai_task_status,
|
||||
parse_video_content_variant, request_body_string, request_body_u32, resolve_follow_up_auth,
|
||||
LocalVideoTaskContentAction, LocalVideoTaskFollowUpPlan, LocalVideoTaskReadResponse,
|
||||
LocalVideoTaskSnapshot, LocalVideoTaskStatus, OpenAiVideoTaskSeed,
|
||||
DEFAULT_VIDEO_TASK_MAX_POLL_COUNT, DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
|
||||
};
|
||||
|
||||
pub fn map_openai_stored_task_to_read_response(
|
||||
task: StoredVideoTask,
|
||||
) -> LocalVideoTaskReadResponse {
|
||||
match task.status {
|
||||
VideoTaskStatus::Cancelled => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task was cancelled"}),
|
||||
},
|
||||
VideoTaskStatus::Deleted => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task not found"}),
|
||||
},
|
||||
status => LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: build_openai_stored_task_body(task, status),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn build_openai_stored_task_body(task: StoredVideoTask, status: VideoTaskStatus) -> Value {
|
||||
let mut body = json!({
|
||||
"id": task.id,
|
||||
"object": "video",
|
||||
"status": map_openai_stored_task_status(status),
|
||||
"progress": task.progress_percent,
|
||||
"created_at": task.created_at_unix_secs,
|
||||
});
|
||||
|
||||
if let Some(model) = task.model {
|
||||
body["model"] = Value::String(model);
|
||||
}
|
||||
if let Some(prompt) = task.prompt {
|
||||
body["prompt"] = Value::String(prompt);
|
||||
}
|
||||
if let Some(size) = task.size {
|
||||
body["size"] = Value::String(size);
|
||||
}
|
||||
if let Some(video_url) = task.video_url {
|
||||
body["video_url"] = Value::String(video_url);
|
||||
}
|
||||
if let Some(completed_at) = task.completed_at_unix_secs {
|
||||
body["completed_at"] = Value::Number(completed_at.into());
|
||||
}
|
||||
if matches!(
|
||||
status,
|
||||
VideoTaskStatus::Failed | VideoTaskStatus::Expired | VideoTaskStatus::Cancelled
|
||||
) {
|
||||
body["error"] = json!({
|
||||
"code": task.error_code.unwrap_or_else(|| "unknown".to_string()),
|
||||
"message": task
|
||||
.error_message
|
||||
.unwrap_or_else(|| "Video generation failed".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
fn map_openai_stored_task_status(status: VideoTaskStatus) -> &'static str {
|
||||
match status {
|
||||
VideoTaskStatus::Pending | VideoTaskStatus::Submitted | VideoTaskStatus::Queued => "queued",
|
||||
VideoTaskStatus::Processing => "processing",
|
||||
VideoTaskStatus::Completed => "completed",
|
||||
VideoTaskStatus::Failed | VideoTaskStatus::Cancelled | VideoTaskStatus::Expired => "failed",
|
||||
VideoTaskStatus::Deleted => "deleted",
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAiVideoTaskSeed {
|
||||
pub fn apply_provider_body(&mut self, provider_body: &Map<String, Value>) {
|
||||
let raw_status = provider_body
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
self.status = match raw_status {
|
||||
"queued" => LocalVideoTaskStatus::Queued,
|
||||
"processing" => LocalVideoTaskStatus::Processing,
|
||||
"completed" => LocalVideoTaskStatus::Completed,
|
||||
"failed" => LocalVideoTaskStatus::Failed,
|
||||
"cancelled" => LocalVideoTaskStatus::Cancelled,
|
||||
"expired" => LocalVideoTaskStatus::Expired,
|
||||
_ => LocalVideoTaskStatus::Submitted,
|
||||
};
|
||||
self.progress_percent = provider_body
|
||||
.get("progress")
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|value| u16::try_from(value).ok())
|
||||
.unwrap_or(match self.status {
|
||||
LocalVideoTaskStatus::Completed => 100,
|
||||
LocalVideoTaskStatus::Processing => 50,
|
||||
_ => self.progress_percent,
|
||||
});
|
||||
self.completed_at_unix_secs = provider_body.get("completed_at").and_then(Value::as_u64);
|
||||
self.expires_at_unix_secs = provider_body.get("expires_at").and_then(Value::as_u64);
|
||||
let error = provider_body.get("error").and_then(Value::as_object);
|
||||
self.error_code = error
|
||||
.and_then(|value| value.get("code"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
self.error_message = error
|
||||
.and_then(|value| value.get("message"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
self.video_url = provider_body
|
||||
.get("video_url")
|
||||
.or_else(|| provider_body.get("url"))
|
||||
.or_else(|| provider_body.get("result_url"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
}
|
||||
|
||||
pub fn build_content_stream_action(
|
||||
&self,
|
||||
query_string: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskContentAction> {
|
||||
match self.status {
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing => {
|
||||
return Some(LocalVideoTaskContentAction::Immediate {
|
||||
status_code: 202,
|
||||
body_json: json!({
|
||||
"detail": format!(
|
||||
"Video is still processing (status: {})",
|
||||
map_openai_task_status(self.status)
|
||||
)
|
||||
}),
|
||||
});
|
||||
}
|
||||
LocalVideoTaskStatus::Failed | LocalVideoTaskStatus::Expired => {
|
||||
return Some(LocalVideoTaskContentAction::Immediate {
|
||||
status_code: 422,
|
||||
body_json: json!({
|
||||
"detail": format!(
|
||||
"Video generation failed: {}",
|
||||
self.error_message
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unknown error".to_string())
|
||||
)
|
||||
}),
|
||||
});
|
||||
}
|
||||
LocalVideoTaskStatus::Cancelled => {
|
||||
return Some(LocalVideoTaskContentAction::Immediate {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task was cancelled"}),
|
||||
});
|
||||
}
|
||||
LocalVideoTaskStatus::Deleted => {
|
||||
return Some(LocalVideoTaskContentAction::Immediate {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task not found"}),
|
||||
});
|
||||
}
|
||||
LocalVideoTaskStatus::Completed => {}
|
||||
}
|
||||
|
||||
let variant = parse_video_content_variant(query_string)?;
|
||||
let (url, headers) = if variant == "video" {
|
||||
if let Some(video_url) = self
|
||||
.video_url
|
||||
.clone()
|
||||
.filter(|value| value.starts_with("http://") || value.starts_with("https://"))
|
||||
{
|
||||
(video_url, BTreeMap::new())
|
||||
} else {
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-type");
|
||||
headers.remove("content-length");
|
||||
(
|
||||
format!(
|
||||
"{}/v1/videos/{}/content",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
self.upstream_task_id
|
||||
),
|
||||
headers,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-type");
|
||||
headers.remove("content-length");
|
||||
(
|
||||
format!(
|
||||
"{}/v1/videos/{}/content?variant={variant}",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
self.upstream_task_id
|
||||
),
|
||||
headers,
|
||||
)
|
||||
};
|
||||
|
||||
Some(LocalVideoTaskContentAction::StreamPlan(ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "GET".to_string(),
|
||||
url,
|
||||
headers,
|
||||
content_type: None,
|
||||
content_encoding: None,
|
||||
body: RequestBody {
|
||||
json_body: None,
|
||||
body_bytes_b64: None,
|
||||
body_ref: None,
|
||||
},
|
||||
stream: true,
|
||||
client_api_format: "openai:video".to_string(),
|
||||
provider_api_format: "openai:video".to_string(),
|
||||
model_name: self
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| self.transport.model_name.clone()),
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn client_body_json(&self) -> Value {
|
||||
let mut body = json!({
|
||||
"id": self.local_task_id,
|
||||
"object": "video",
|
||||
"status": map_openai_task_status(self.status),
|
||||
"progress": self.progress_percent,
|
||||
"created_at": self.created_at_unix_secs,
|
||||
});
|
||||
|
||||
if let Some(model) = &self.model {
|
||||
body["model"] = Value::String(model.clone());
|
||||
}
|
||||
if let Some(prompt) = &self.prompt {
|
||||
body["prompt"] = Value::String(prompt.clone());
|
||||
}
|
||||
if let Some(size) = &self.size {
|
||||
body["size"] = Value::String(size.clone());
|
||||
}
|
||||
if let Some(seconds) = &self.seconds {
|
||||
body["seconds"] = Value::String(seconds.clone());
|
||||
}
|
||||
if let Some(remixed_from_video_id) = &self.remixed_from_video_id {
|
||||
body["remixed_from_video_id"] = Value::String(remixed_from_video_id.clone());
|
||||
}
|
||||
if let Some(completed_at) = self.completed_at_unix_secs {
|
||||
body["completed_at"] = Value::Number(completed_at.into());
|
||||
}
|
||||
if let Some(expires_at) = self.expires_at_unix_secs {
|
||||
body["expires_at"] = Value::Number(expires_at.into());
|
||||
}
|
||||
if self.status == LocalVideoTaskStatus::Failed
|
||||
|| self.status == LocalVideoTaskStatus::Expired
|
||||
{
|
||||
body["error"] = json!({
|
||||
"code": self.error_code.clone().unwrap_or_else(|| "unknown".to_string()),
|
||||
"message": self
|
||||
.error_message
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Video generation failed".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
pub fn build_delete_follow_up_plan(
|
||||
&self,
|
||||
fallback_user_id: Option<&str>,
|
||||
fallback_api_key_id: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskFollowUpPlan> {
|
||||
if !matches!(
|
||||
self.status,
|
||||
LocalVideoTaskStatus::Completed | LocalVideoTaskStatus::Failed
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let (user_id, api_key_id) = resolve_follow_up_auth(
|
||||
self.user_id.as_deref(),
|
||||
self.api_key_id.as_deref(),
|
||||
fallback_user_id,
|
||||
fallback_api_key_id,
|
||||
)?;
|
||||
let model_name = self
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| self.transport.model_name.clone());
|
||||
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-type");
|
||||
headers.remove("content-length");
|
||||
|
||||
Some(LocalVideoTaskFollowUpPlan {
|
||||
plan: ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "DELETE".to_string(),
|
||||
url: format!(
|
||||
"{}/v1/videos/{}",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
self.upstream_task_id
|
||||
),
|
||||
headers,
|
||||
content_type: None,
|
||||
content_encoding: None,
|
||||
body: RequestBody {
|
||||
json_body: None,
|
||||
body_bytes_b64: None,
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "openai:video".to_string(),
|
||||
provider_api_format: "openai:video".to_string(),
|
||||
model_name: model_name.clone(),
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
},
|
||||
report_kind: Some("openai_video_delete_sync_finalize".to_string()),
|
||||
report_context: Some(build_video_follow_up_report_context(
|
||||
&self.persistence.request_id,
|
||||
&user_id,
|
||||
&api_key_id,
|
||||
&self.local_task_id,
|
||||
&self.transport.provider_id,
|
||||
&self.transport.endpoint_id,
|
||||
&self.transport.key_id,
|
||||
self.transport.provider_name.as_deref(),
|
||||
model_name.as_deref(),
|
||||
"openai:video",
|
||||
"openai:video",
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_get_follow_up_plan(&self, trace_id: &str) -> Option<ExecutionPlan> {
|
||||
if !matches!(
|
||||
self.status,
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-type");
|
||||
headers.remove("content-length");
|
||||
|
||||
Some(ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "GET".to_string(),
|
||||
url: format!(
|
||||
"{}/v1/videos/{}",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
self.upstream_task_id
|
||||
),
|
||||
headers,
|
||||
content_type: None,
|
||||
content_encoding: None,
|
||||
body: RequestBody {
|
||||
json_body: None,
|
||||
body_bytes_b64: None,
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "openai:video".to_string(),
|
||||
provider_api_format: "openai:video".to_string(),
|
||||
model_name: self
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| self.transport.model_name.clone()),
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_cancel_follow_up_plan(
|
||||
&self,
|
||||
fallback_user_id: Option<&str>,
|
||||
fallback_api_key_id: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskFollowUpPlan> {
|
||||
if !matches!(
|
||||
self.status,
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let (user_id, api_key_id) = resolve_follow_up_auth(
|
||||
self.user_id.as_deref(),
|
||||
self.api_key_id.as_deref(),
|
||||
fallback_user_id,
|
||||
fallback_api_key_id,
|
||||
)?;
|
||||
let model_name = self
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| self.transport.model_name.clone());
|
||||
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-type");
|
||||
headers.remove("content-length");
|
||||
|
||||
Some(LocalVideoTaskFollowUpPlan {
|
||||
plan: ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "DELETE".to_string(),
|
||||
url: format!(
|
||||
"{}/v1/videos/{}",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
self.upstream_task_id
|
||||
),
|
||||
headers,
|
||||
content_type: None,
|
||||
content_encoding: None,
|
||||
body: RequestBody {
|
||||
json_body: None,
|
||||
body_bytes_b64: None,
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "openai:video".to_string(),
|
||||
provider_api_format: "openai:video".to_string(),
|
||||
model_name: model_name.clone(),
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
},
|
||||
report_kind: Some("openai_video_cancel_sync_finalize".to_string()),
|
||||
report_context: Some(build_video_follow_up_report_context(
|
||||
&self.persistence.request_id,
|
||||
&user_id,
|
||||
&api_key_id,
|
||||
&self.local_task_id,
|
||||
&self.transport.provider_id,
|
||||
&self.transport.endpoint_id,
|
||||
&self.transport.key_id,
|
||||
self.transport.provider_name.as_deref(),
|
||||
model_name.as_deref(),
|
||||
"openai:video",
|
||||
"openai:video",
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_remix_follow_up_plan(
|
||||
&self,
|
||||
body_json: &Value,
|
||||
fallback_user_id: Option<&str>,
|
||||
fallback_api_key_id: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskFollowUpPlan> {
|
||||
if !matches!(self.status, LocalVideoTaskStatus::Completed) || body_json.is_null() {
|
||||
return None;
|
||||
}
|
||||
let (user_id, api_key_id) = resolve_follow_up_auth(
|
||||
self.user_id.as_deref(),
|
||||
self.api_key_id.as_deref(),
|
||||
fallback_user_id,
|
||||
fallback_api_key_id,
|
||||
)?;
|
||||
let model_name = self
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| self.transport.model_name.clone());
|
||||
|
||||
let mut headers = self.transport.headers.clone();
|
||||
headers.remove("content-length");
|
||||
let content_type = self
|
||||
.transport
|
||||
.content_type
|
||||
.clone()
|
||||
.unwrap_or_else(|| "application/json".to_string());
|
||||
headers
|
||||
.entry("content-type".to_string())
|
||||
.or_insert_with(|| content_type.clone());
|
||||
|
||||
let mut report_context = build_video_follow_up_report_context(
|
||||
&self.persistence.request_id,
|
||||
&user_id,
|
||||
&api_key_id,
|
||||
&self.local_task_id,
|
||||
&self.transport.provider_id,
|
||||
&self.transport.endpoint_id,
|
||||
&self.transport.key_id,
|
||||
self.transport.provider_name.as_deref(),
|
||||
model_name.as_deref(),
|
||||
"openai:video",
|
||||
"openai:video",
|
||||
);
|
||||
if let Some(report_context_object) = report_context.as_object_mut() {
|
||||
report_context_object.insert("original_request_body".to_string(), body_json.clone());
|
||||
}
|
||||
|
||||
Some(LocalVideoTaskFollowUpPlan {
|
||||
plan: ExecutionPlan {
|
||||
request_id: trace_id.to_string(),
|
||||
candidate_id: None,
|
||||
provider_name: self.transport.provider_name.clone(),
|
||||
provider_id: self.transport.provider_id.clone(),
|
||||
endpoint_id: self.transport.endpoint_id.clone(),
|
||||
key_id: self.transport.key_id.clone(),
|
||||
method: "POST".to_string(),
|
||||
url: format!(
|
||||
"{}/v1/videos/{}/remix",
|
||||
self.transport.upstream_base_url.trim_end_matches('/'),
|
||||
self.upstream_task_id
|
||||
),
|
||||
headers,
|
||||
content_type: Some(content_type),
|
||||
content_encoding: None,
|
||||
body: RequestBody::from_json(body_json.clone()),
|
||||
stream: false,
|
||||
client_api_format: "openai:video".to_string(),
|
||||
provider_api_format: "openai:video".to_string(),
|
||||
model_name,
|
||||
proxy: self.transport.proxy.clone(),
|
||||
tls_profile: self.transport.tls_profile.clone(),
|
||||
timeouts: self.transport.timeouts.clone(),
|
||||
},
|
||||
report_kind: Some("openai_video_remix_sync_finalize".to_string()),
|
||||
report_context: Some(report_context),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_upsert_record(&self) -> UpsertVideoTask {
|
||||
let now_unix_secs = current_unix_timestamp_secs();
|
||||
let next_poll_at_unix_secs = match self.status {
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing => Some(
|
||||
self.created_at_unix_secs
|
||||
.saturating_add(u64::from(DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS)),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
UpsertVideoTask {
|
||||
id: self.local_task_id.clone(),
|
||||
short_id: None,
|
||||
request_id: self.persistence.request_id.clone(),
|
||||
user_id: self.user_id.clone(),
|
||||
api_key_id: self.api_key_id.clone(),
|
||||
username: self.persistence.username.clone(),
|
||||
api_key_name: self.persistence.api_key_name.clone(),
|
||||
external_task_id: Some(self.upstream_task_id.clone()),
|
||||
provider_id: Some(self.transport.provider_id.clone()),
|
||||
endpoint_id: Some(self.transport.endpoint_id.clone()),
|
||||
key_id: Some(self.transport.key_id.clone()),
|
||||
client_api_format: Some(self.persistence.client_api_format.clone()),
|
||||
provider_api_format: Some(self.persistence.provider_api_format.clone()),
|
||||
format_converted: self.persistence.format_converted,
|
||||
model: self.model.clone().or_else(|| Some(String::new())),
|
||||
prompt: self.prompt.clone().or_else(|| Some(String::new())),
|
||||
original_request_body: Some(self.persistence.original_request_body.clone()),
|
||||
duration_seconds: request_body_u32(&self.persistence.original_request_body, "seconds"),
|
||||
resolution: request_body_string(&self.persistence.original_request_body, "resolution"),
|
||||
aspect_ratio: request_body_string(
|
||||
&self.persistence.original_request_body,
|
||||
"aspect_ratio",
|
||||
),
|
||||
size: self.size.clone(),
|
||||
status: self.status.as_database_status(),
|
||||
progress_percent: self.progress_percent,
|
||||
progress_message: None,
|
||||
retry_count: 0,
|
||||
poll_interval_seconds: DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
|
||||
next_poll_at_unix_secs,
|
||||
poll_count: 0,
|
||||
max_poll_count: DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
|
||||
created_at_unix_secs: self.created_at_unix_secs,
|
||||
submitted_at_unix_secs: Some(self.created_at_unix_secs),
|
||||
completed_at_unix_secs: self.completed_at_unix_secs,
|
||||
updated_at_unix_secs: self.completed_at_unix_secs.unwrap_or(now_unix_secs),
|
||||
error_code: self.error_code.clone(),
|
||||
error_message: self.error_message.clone(),
|
||||
video_url: self.video_url.clone(),
|
||||
request_metadata: Some(json!({
|
||||
"rust_owner": "async_task",
|
||||
"rust_local_snapshot": LocalVideoTaskSnapshot::OpenAi(self.clone()),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aether_data::repository::video_tasks::{StoredVideoTask, VideoTaskStatus};
|
||||
|
||||
use super::map_openai_stored_task_to_read_response;
|
||||
|
||||
fn sample_stored_task(status: VideoTaskStatus) -> StoredVideoTask {
|
||||
StoredVideoTask {
|
||||
id: "task-openai-123".to_string(),
|
||||
short_id: None,
|
||||
request_id: "req-openai-123".to_string(),
|
||||
user_id: None,
|
||||
api_key_id: None,
|
||||
username: None,
|
||||
api_key_name: None,
|
||||
external_task_id: Some("ext-openai-123".to_string()),
|
||||
provider_id: None,
|
||||
endpoint_id: None,
|
||||
key_id: None,
|
||||
client_api_format: Some("openai:video".to_string()),
|
||||
provider_api_format: Some("openai:video".to_string()),
|
||||
format_converted: false,
|
||||
model: Some("sora-2".to_string()),
|
||||
prompt: Some("hello".to_string()),
|
||||
original_request_body: None,
|
||||
duration_seconds: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
size: Some("1280x720".to_string()),
|
||||
status,
|
||||
progress_percent: 100,
|
||||
progress_message: None,
|
||||
retry_count: 0,
|
||||
poll_interval_seconds: 10,
|
||||
next_poll_at_unix_secs: None,
|
||||
poll_count: 0,
|
||||
max_poll_count: 360,
|
||||
created_at_unix_secs: 1712345678,
|
||||
submitted_at_unix_secs: Some(1712345678),
|
||||
completed_at_unix_secs: Some(1712345688),
|
||||
updated_at_unix_secs: 1712345688,
|
||||
error_code: Some("upstream_failed".to_string()),
|
||||
error_message: Some("provider failed".to_string()),
|
||||
video_url: Some("https://cdn.example.com/video.mp4".to_string()),
|
||||
request_metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_openai_failed_stored_task_into_read_response() {
|
||||
let response =
|
||||
map_openai_stored_task_to_read_response(sample_stored_task(VideoTaskStatus::Failed));
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(response.body_json["id"], "task-openai-123");
|
||||
assert_eq!(response.body_json["status"], "failed");
|
||||
assert_eq!(response.body_json["completed_at"], 1712345688u64);
|
||||
assert_eq!(response.body_json["error"]["code"], "upstream_failed");
|
||||
assert_eq!(
|
||||
response.body_json["video_url"],
|
||||
"https://cdn.example.com/video.mp4"
|
||||
);
|
||||
}
|
||||
}
|
||||
410
crates/aether-video-tasks-core/src/path.rs
Normal file
410
crates/aether-video-tasks-core/src/path.rs
Normal file
@@ -0,0 +1,410 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use aether_data::repository::video_tasks::VideoTaskLookupKey;
|
||||
use aether_data::repository::video_tasks::VideoTaskStatus as StoredVideoTaskStatus;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{LocalVideoTaskRegistryMutation, LocalVideoTaskStatus, VideoTaskTruthSourceMode};
|
||||
|
||||
pub fn extract_openai_task_id_from_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
if suffix.is_empty()
|
||||
|| suffix.contains('/')
|
||||
|| suffix.ends_with(":cancel")
|
||||
|| suffix.ends_with(":delete")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(suffix)
|
||||
}
|
||||
|
||||
pub fn extract_gemini_short_id_from_path(path: &str) -> Option<&str> {
|
||||
let operations_index = path.find("/operations/")?;
|
||||
let suffix = &path[(operations_index + "/operations/".len())..];
|
||||
if suffix.is_empty() || suffix.contains('/') || suffix.ends_with(":cancel") {
|
||||
return None;
|
||||
}
|
||||
Some(suffix)
|
||||
}
|
||||
|
||||
pub fn extract_openai_task_id_from_cancel_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
suffix
|
||||
.strip_suffix("/cancel")
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn extract_openai_task_id_from_remix_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
suffix
|
||||
.strip_suffix("/remix")
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn extract_openai_task_id_from_content_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
suffix
|
||||
.strip_suffix("/content")
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn extract_gemini_short_id_from_cancel_path(path: &str) -> Option<&str> {
|
||||
let operations_index = path.find("/operations/")?;
|
||||
let suffix = &path[(operations_index + "/operations/".len())..];
|
||||
let short_id = suffix.strip_suffix(":cancel")?;
|
||||
if short_id.is_empty() || short_id.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(short_id)
|
||||
}
|
||||
|
||||
pub fn resolve_video_task_read_lookup_key<'a>(
|
||||
route_family: Option<&str>,
|
||||
request_path: &'a str,
|
||||
) -> Option<VideoTaskLookupKey<'a>> {
|
||||
match route_family {
|
||||
Some("openai") => {
|
||||
extract_openai_task_id_from_path(request_path).map(VideoTaskLookupKey::Id)
|
||||
}
|
||||
Some("gemini") => {
|
||||
extract_gemini_short_id_from_path(request_path).map(VideoTaskLookupKey::ShortId)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_video_task_hydration_lookup_key<'a>(
|
||||
route_family: Option<&str>,
|
||||
request_path: &'a str,
|
||||
) -> Option<VideoTaskLookupKey<'a>> {
|
||||
match route_family {
|
||||
Some("openai") => extract_openai_task_id_from_path(request_path)
|
||||
.or_else(|| extract_openai_task_id_from_cancel_path(request_path))
|
||||
.or_else(|| extract_openai_task_id_from_remix_path(request_path))
|
||||
.or_else(|| extract_openai_task_id_from_content_path(request_path))
|
||||
.map(VideoTaskLookupKey::Id),
|
||||
Some("gemini") => extract_gemini_short_id_from_path(request_path)
|
||||
.or_else(|| extract_gemini_short_id_from_cancel_path(request_path))
|
||||
.map(VideoTaskLookupKey::ShortId),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VideoTaskReportLookup<'a> {
|
||||
Lookup(VideoTaskLookupKey<'a>),
|
||||
TaskIdOrExternal {
|
||||
task_id: &'a str,
|
||||
user_id: Option<&'a str>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn resolve_video_task_report_lookup<'a>(
|
||||
context: &'a Value,
|
||||
) -> Option<VideoTaskReportLookup<'a>> {
|
||||
if let Some(task_id) = non_empty_context_str(context, "local_task_id") {
|
||||
return Some(VideoTaskReportLookup::Lookup(VideoTaskLookupKey::Id(
|
||||
task_id,
|
||||
)));
|
||||
}
|
||||
if let Some(short_id) = non_empty_context_str(context, "local_short_id") {
|
||||
return Some(VideoTaskReportLookup::Lookup(VideoTaskLookupKey::ShortId(
|
||||
short_id,
|
||||
)));
|
||||
}
|
||||
|
||||
let task_id = non_empty_context_str(context, "task_id")?;
|
||||
Some(VideoTaskReportLookup::TaskIdOrExternal {
|
||||
task_id,
|
||||
user_id: non_empty_context_str(context, "user_id"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_local_sync_finalize_request_path(
|
||||
report_kind: &str,
|
||||
signature: &str,
|
||||
report_context: Option<&Value>,
|
||||
) -> Option<String> {
|
||||
match report_kind {
|
||||
"openai_video_delete_sync_finalize" => {
|
||||
let task_id =
|
||||
report_context.and_then(|value| non_empty_context_str(value, "task_id"))?;
|
||||
Some(format!("/v1/videos/{task_id}"))
|
||||
}
|
||||
"openai_video_cancel_sync_finalize" => {
|
||||
let task_id =
|
||||
report_context.and_then(|value| non_empty_context_str(value, "task_id"))?;
|
||||
Some(format!("/v1/videos/{task_id}/cancel"))
|
||||
}
|
||||
"gemini_video_cancel_sync_finalize" => {
|
||||
let short_id = report_context
|
||||
.and_then(|value| non_empty_context_str(value, "task_id"))
|
||||
.or_else(|| {
|
||||
report_context.and_then(|value| non_empty_context_str(value, "local_short_id"))
|
||||
})
|
||||
.or_else(|| {
|
||||
report_context
|
||||
.and_then(|value| non_empty_context_str(value, "operation_name"))
|
||||
.and_then(|value| value.rsplit('/').next())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
})?;
|
||||
let model = report_context
|
||||
.and_then(|value| non_empty_context_str(value, "model"))
|
||||
.or_else(|| {
|
||||
report_context.and_then(|value| non_empty_context_str(value, "model_name"))
|
||||
})
|
||||
.unwrap_or(match signature {
|
||||
"gemini:video" => "veo-3",
|
||||
_ => "unknown",
|
||||
});
|
||||
Some(format!(
|
||||
"/v1beta/models/{model}/operations/{short_id}:cancel"
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_local_video_registry_mutation(
|
||||
truth_source_mode: VideoTaskTruthSourceMode,
|
||||
request_path: &str,
|
||||
report_kind: &str,
|
||||
) -> Option<LocalVideoTaskRegistryMutation> {
|
||||
if truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return None;
|
||||
}
|
||||
|
||||
match report_kind {
|
||||
"openai_video_delete_sync_finalize" => {
|
||||
let task_id = extract_openai_task_id_from_path(request_path)?;
|
||||
Some(LocalVideoTaskRegistryMutation::OpenAiDeleted {
|
||||
task_id: task_id.to_string(),
|
||||
})
|
||||
}
|
||||
"openai_video_cancel_sync_finalize" => {
|
||||
let task_id = extract_openai_task_id_from_cancel_path(request_path)?;
|
||||
Some(LocalVideoTaskRegistryMutation::OpenAiCancelled {
|
||||
task_id: task_id.to_string(),
|
||||
})
|
||||
}
|
||||
"gemini_video_cancel_sync_finalize" => {
|
||||
let short_id = extract_gemini_short_id_from_cancel_path(request_path)?;
|
||||
Some(LocalVideoTaskRegistryMutation::GeminiCancelled {
|
||||
short_id: short_id.to_string(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_unix_timestamp_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
pub fn generate_local_short_id() -> String {
|
||||
Uuid::new_v4()
|
||||
.simple()
|
||||
.to_string()
|
||||
.chars()
|
||||
.take(12)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn local_status_from_stored(status: StoredVideoTaskStatus) -> LocalVideoTaskStatus {
|
||||
match status {
|
||||
StoredVideoTaskStatus::Pending | StoredVideoTaskStatus::Submitted => {
|
||||
LocalVideoTaskStatus::Submitted
|
||||
}
|
||||
StoredVideoTaskStatus::Queued => LocalVideoTaskStatus::Queued,
|
||||
StoredVideoTaskStatus::Processing => LocalVideoTaskStatus::Processing,
|
||||
StoredVideoTaskStatus::Completed => LocalVideoTaskStatus::Completed,
|
||||
StoredVideoTaskStatus::Failed => LocalVideoTaskStatus::Failed,
|
||||
StoredVideoTaskStatus::Cancelled => LocalVideoTaskStatus::Cancelled,
|
||||
StoredVideoTaskStatus::Expired => LocalVideoTaskStatus::Expired,
|
||||
StoredVideoTaskStatus::Deleted => LocalVideoTaskStatus::Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_context_str<'a>(context: &'a Value, key: &str) -> Option<&'a str> {
|
||||
context
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_local_sync_finalize_request_path, extract_gemini_short_id_from_cancel_path,
|
||||
extract_gemini_short_id_from_path, extract_openai_task_id_from_cancel_path,
|
||||
extract_openai_task_id_from_content_path, extract_openai_task_id_from_path,
|
||||
extract_openai_task_id_from_remix_path, generate_local_short_id,
|
||||
resolve_video_task_hydration_lookup_key, resolve_video_task_read_lookup_key,
|
||||
resolve_video_task_report_lookup, VideoTaskReportLookup,
|
||||
};
|
||||
use aether_data::repository::video_tasks::VideoTaskLookupKey;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn path_extractors_handle_supported_video_routes() {
|
||||
assert_eq!(
|
||||
extract_openai_task_id_from_path("/v1/videos/task_123"),
|
||||
Some("task_123")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_openai_task_id_from_cancel_path("/v1/videos/task_123/cancel"),
|
||||
Some("task_123")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_openai_task_id_from_remix_path("/v1/videos/task_123/remix"),
|
||||
Some("task_123")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_openai_task_id_from_content_path("/v1/videos/task_123/content"),
|
||||
Some("task_123")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_gemini_short_id_from_path("/v1beta/models/foo/operations/abc123"),
|
||||
Some("abc123")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_gemini_short_id_from_cancel_path("/v1beta/models/foo/operations/abc123:cancel"),
|
||||
Some("abc123")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_short_id_has_expected_length() {
|
||||
assert_eq!(generate_local_short_id().len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_video_task_read_lookup_key_for_supported_read_paths() {
|
||||
assert_eq!(
|
||||
resolve_video_task_read_lookup_key(Some("openai"), "/v1/videos/task_123"),
|
||||
Some(VideoTaskLookupKey::Id("task_123"))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_video_task_read_lookup_key(
|
||||
Some("gemini"),
|
||||
"/v1beta/models/foo/operations/abc123"
|
||||
),
|
||||
Some(VideoTaskLookupKey::ShortId("abc123"))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_video_task_read_lookup_key(Some("openai"), "/v1/videos/task_123/cancel"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_video_task_hydration_lookup_key_for_supported_follow_up_paths() {
|
||||
assert_eq!(
|
||||
resolve_video_task_hydration_lookup_key(Some("openai"), "/v1/videos/task_123/cancel"),
|
||||
Some(VideoTaskLookupKey::Id("task_123"))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_video_task_hydration_lookup_key(Some("openai"), "/v1/videos/task_123/remix"),
|
||||
Some(VideoTaskLookupKey::Id("task_123"))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_video_task_hydration_lookup_key(
|
||||
Some("gemini"),
|
||||
"/v1beta/models/foo/operations/abc123:cancel"
|
||||
),
|
||||
Some(VideoTaskLookupKey::ShortId("abc123"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_video_task_report_lookup_for_supported_context_shapes() {
|
||||
assert_eq!(
|
||||
resolve_video_task_report_lookup(&json!({
|
||||
"local_task_id": "task-local-123"
|
||||
})),
|
||||
Some(VideoTaskReportLookup::Lookup(VideoTaskLookupKey::Id(
|
||||
"task-local-123"
|
||||
)))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_video_task_report_lookup(&json!({
|
||||
"local_short_id": "short-local-123"
|
||||
})),
|
||||
Some(VideoTaskReportLookup::Lookup(VideoTaskLookupKey::ShortId(
|
||||
"short-local-123"
|
||||
)))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_video_task_report_lookup(&json!({
|
||||
"task_id": "task-upstream-123",
|
||||
"user_id": "user-123"
|
||||
})),
|
||||
Some(VideoTaskReportLookup::TaskIdOrExternal {
|
||||
task_id: "task-upstream-123",
|
||||
user_id: Some("user-123"),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_local_sync_finalize_request_path_for_supported_video_finalize_kinds() {
|
||||
assert_eq!(
|
||||
build_local_sync_finalize_request_path(
|
||||
"openai_video_delete_sync_finalize",
|
||||
"openai:video",
|
||||
Some(&json!({"task_id": "task_123"})),
|
||||
),
|
||||
Some("/v1/videos/task_123".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
build_local_sync_finalize_request_path(
|
||||
"openai_video_cancel_sync_finalize",
|
||||
"openai:video",
|
||||
Some(&json!({"task_id": "task_123"})),
|
||||
),
|
||||
Some("/v1/videos/task_123/cancel".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
build_local_sync_finalize_request_path(
|
||||
"gemini_video_cancel_sync_finalize",
|
||||
"gemini:video",
|
||||
Some(&json!({"operation_name": "models/veo-3/operations/abc123"})),
|
||||
),
|
||||
Some("/v1beta/models/veo-3/operations/abc123:cancel".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_local_sync_finalize_request_path_when_context_is_invalid() {
|
||||
assert_eq!(
|
||||
build_local_sync_finalize_request_path(
|
||||
"openai_video_delete_sync_finalize",
|
||||
"openai:video",
|
||||
Some(&json!({})),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
build_local_sync_finalize_request_path(
|
||||
"gemini_video_cancel_sync_finalize",
|
||||
"gemini:video",
|
||||
Some(&json!({"task_id": ""})),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
build_local_sync_finalize_request_path(
|
||||
"unknown_finalize_kind",
|
||||
"gemini:video",
|
||||
Some(&json!({"task_id": "abc123"})),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
66
crates/aether-video-tasks-core/src/read_side.rs
Normal file
66
crates/aether-video-tasks-core/src/read_side.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use aether_data::repository::video_tasks::{StoredVideoTask, VideoTaskLookupKey};
|
||||
use aether_data::DataLayerError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
map_gemini_stored_task_to_read_response, map_openai_stored_task_to_read_response,
|
||||
resolve_video_task_read_lookup_key, LocalVideoTaskReadResponse,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait StoredVideoTaskReadSide: Send + Sync {
|
||||
async fn find_stored_video_task(
|
||||
&self,
|
||||
key: VideoTaskLookupKey<'_>,
|
||||
) -> Result<Option<StoredVideoTask>, DataLayerError>;
|
||||
}
|
||||
|
||||
pub async fn read_data_backed_video_task_response(
|
||||
state: &impl StoredVideoTaskReadSide,
|
||||
route_family: Option<&str>,
|
||||
request_path: &str,
|
||||
) -> Result<Option<LocalVideoTaskReadResponse>, DataLayerError> {
|
||||
match route_family {
|
||||
Some("openai") => read_openai_video_task_response(state, request_path).await,
|
||||
Some("gemini") => read_gemini_video_task_response(state, request_path).await,
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_openai_video_task_response(
|
||||
state: &impl StoredVideoTaskReadSide,
|
||||
request_path: &str,
|
||||
) -> Result<Option<LocalVideoTaskReadResponse>, DataLayerError> {
|
||||
let Some(lookup) = resolve_video_task_read_lookup_key(Some("openai"), request_path) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(task) = state.find_stored_video_task(lookup).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if !matches!(task.provider_api_format.as_deref(), Some("openai:video")) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(map_openai_stored_task_to_read_response(task)))
|
||||
}
|
||||
|
||||
async fn read_gemini_video_task_response(
|
||||
state: &impl StoredVideoTaskReadSide,
|
||||
request_path: &str,
|
||||
) -> Result<Option<LocalVideoTaskReadResponse>, DataLayerError> {
|
||||
let Some(lookup) = resolve_video_task_read_lookup_key(Some("gemini"), request_path) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(task) = state.find_stored_video_task(lookup).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if !matches!(task.provider_api_format.as_deref(), Some("gemini:video")) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(map_gemini_stored_task_to_read_response(task)))
|
||||
}
|
||||
330
crates/aether-video-tasks-core/src/service.rs
Normal file
330
crates/aether-video-tasks-core/src/service.rs
Normal file
@@ -0,0 +1,330 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aether_contracts::ExecutionPlan;
|
||||
use aether_data::repository::video_tasks::StoredVideoTask;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{
|
||||
extract_gemini_short_id_from_cancel_path, extract_gemini_short_id_from_path,
|
||||
extract_openai_task_id_from_cancel_path, extract_openai_task_id_from_content_path,
|
||||
extract_openai_task_id_from_path, extract_openai_task_id_from_remix_path,
|
||||
resolve_local_video_registry_mutation, FileVideoTaskStore, InMemoryVideoTaskStore,
|
||||
LocalVideoTaskContentAction, LocalVideoTaskFollowUpPlan, LocalVideoTaskProjectionTarget,
|
||||
LocalVideoTaskReadRefreshPlan, LocalVideoTaskReadResponse, LocalVideoTaskSnapshot,
|
||||
LocalVideoTaskSuccessPlan, VideoTaskStore, VideoTaskTruthSourceMode,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VideoTaskService {
|
||||
truth_source_mode: VideoTaskTruthSourceMode,
|
||||
store: Arc<dyn VideoTaskStore>,
|
||||
}
|
||||
|
||||
impl VideoTaskService {
|
||||
pub fn new(mode: VideoTaskTruthSourceMode) -> Self {
|
||||
Self::with_store(mode, Arc::new(InMemoryVideoTaskStore::default()))
|
||||
}
|
||||
|
||||
pub fn with_file_store(
|
||||
mode: VideoTaskTruthSourceMode,
|
||||
path: impl Into<PathBuf>,
|
||||
) -> std::io::Result<Self> {
|
||||
Ok(Self::with_store(
|
||||
mode,
|
||||
Arc::new(FileVideoTaskStore::new(path)?),
|
||||
))
|
||||
}
|
||||
|
||||
fn with_store(mode: VideoTaskTruthSourceMode, store: Arc<dyn VideoTaskStore>) -> Self {
|
||||
Self {
|
||||
truth_source_mode: mode,
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_truth_source_mode(&self, mode: VideoTaskTruthSourceMode) -> Self {
|
||||
Self {
|
||||
truth_source_mode: mode,
|
||||
store: self.store.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rust_authoritative(&self) -> bool {
|
||||
self.truth_source_mode == VideoTaskTruthSourceMode::RustAuthoritative
|
||||
}
|
||||
|
||||
pub fn truth_source_mode(&self) -> VideoTaskTruthSourceMode {
|
||||
self.truth_source_mode
|
||||
}
|
||||
|
||||
pub fn prepare_sync_success(
|
||||
&self,
|
||||
report_kind: &str,
|
||||
provider_body: &Map<String, Value>,
|
||||
report_context: &Map<String, Value>,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Option<LocalVideoTaskSuccessPlan> {
|
||||
self.truth_source_mode.prepare_sync_success(
|
||||
report_kind,
|
||||
provider_body,
|
||||
report_context,
|
||||
plan,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn record_snapshot(&self, snapshot: LocalVideoTaskSnapshot) {
|
||||
self.store.insert(snapshot);
|
||||
}
|
||||
|
||||
pub fn hydrate_from_stored_task(&self, task: &StoredVideoTask) -> bool {
|
||||
let Some(snapshot) = LocalVideoTaskSnapshot::from_stored_task(task) else {
|
||||
return false;
|
||||
};
|
||||
self.store.insert(snapshot);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn apply_finalize_mutation(&self, request_path: &str, report_kind: &str) {
|
||||
let Some(mutation) = resolve_local_video_registry_mutation(
|
||||
self.truth_source_mode,
|
||||
request_path,
|
||||
report_kind,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
self.store.apply_mutation(mutation);
|
||||
}
|
||||
|
||||
pub fn read_response(
|
||||
&self,
|
||||
route_family: Option<&str>,
|
||||
request_path: &str,
|
||||
) -> Option<LocalVideoTaskReadResponse> {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return None;
|
||||
}
|
||||
match route_family {
|
||||
Some("openai") => extract_openai_task_id_from_path(request_path)
|
||||
.and_then(|task_id| self.store.read_openai(task_id)),
|
||||
Some("gemini") => extract_gemini_short_id_from_path(request_path)
|
||||
.and_then(|short_id| self.store.read_gemini(short_id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot_for_route(
|
||||
&self,
|
||||
route_family: Option<&str>,
|
||||
request_path: &str,
|
||||
) -> Option<LocalVideoTaskSnapshot> {
|
||||
match route_family {
|
||||
Some("openai") => extract_openai_task_id_from_path(request_path)
|
||||
.and_then(|task_id| self.store.clone_openai(task_id))
|
||||
.map(LocalVideoTaskSnapshot::OpenAi),
|
||||
Some("gemini") => extract_gemini_short_id_from_path(request_path)
|
||||
.and_then(|short_id| self.store.clone_gemini(short_id))
|
||||
.map(LocalVideoTaskSnapshot::Gemini),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_openai_content_stream_action(
|
||||
&self,
|
||||
request_path: &str,
|
||||
query_string: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskContentAction> {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return None;
|
||||
}
|
||||
let task_id = extract_openai_task_id_from_content_path(request_path)?;
|
||||
let seed = self.store.clone_openai(task_id)?;
|
||||
seed.build_content_stream_action(query_string, trace_id)
|
||||
}
|
||||
|
||||
pub fn snapshot_for_refresh_plan(
|
||||
&self,
|
||||
refresh_plan: &LocalVideoTaskReadRefreshPlan,
|
||||
) -> Option<LocalVideoTaskSnapshot> {
|
||||
match &refresh_plan.projection_target {
|
||||
LocalVideoTaskProjectionTarget::OpenAi { task_id } => self
|
||||
.store
|
||||
.clone_openai(task_id)
|
||||
.map(LocalVideoTaskSnapshot::OpenAi),
|
||||
LocalVideoTaskProjectionTarget::Gemini { short_id } => self
|
||||
.store
|
||||
.clone_gemini(short_id)
|
||||
.map(LocalVideoTaskSnapshot::Gemini),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_openai_task_response(
|
||||
&self,
|
||||
task_id: &str,
|
||||
provider_body: &Map<String, Value>,
|
||||
) -> bool {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return false;
|
||||
}
|
||||
self.store.project_openai(task_id, provider_body)
|
||||
}
|
||||
|
||||
pub fn project_gemini_task_response(
|
||||
&self,
|
||||
short_id: &str,
|
||||
provider_body: &Map<String, Value>,
|
||||
) -> bool {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return false;
|
||||
}
|
||||
self.store.project_gemini(short_id, provider_body)
|
||||
}
|
||||
|
||||
pub fn prepare_read_refresh_sync_plan(
|
||||
&self,
|
||||
route_family: Option<&str>,
|
||||
request_path: &str,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskReadRefreshPlan> {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return None;
|
||||
}
|
||||
match route_family {
|
||||
Some("openai") => {
|
||||
let task_id = extract_openai_task_id_from_path(request_path)?;
|
||||
let seed = self.store.clone_openai(task_id)?;
|
||||
Some(LocalVideoTaskReadRefreshPlan {
|
||||
plan: seed.build_get_follow_up_plan(trace_id)?,
|
||||
projection_target: LocalVideoTaskProjectionTarget::OpenAi {
|
||||
task_id: task_id.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
Some("gemini") => {
|
||||
let short_id = extract_gemini_short_id_from_path(request_path)?;
|
||||
let seed = self.store.clone_gemini(short_id)?;
|
||||
Some(LocalVideoTaskReadRefreshPlan {
|
||||
plan: seed.build_get_follow_up_plan(trace_id)?,
|
||||
projection_target: LocalVideoTaskProjectionTarget::Gemini {
|
||||
short_id: short_id.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_poll_refresh_batch(
|
||||
&self,
|
||||
limit: usize,
|
||||
trace_prefix: &str,
|
||||
) -> Vec<LocalVideoTaskReadRefreshPlan> {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative || limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.store
|
||||
.list_active_snapshots(limit)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, snapshot)| {
|
||||
let trace_id = format!("{trace_prefix}-{index}");
|
||||
match snapshot {
|
||||
LocalVideoTaskSnapshot::OpenAi(seed) => Some(LocalVideoTaskReadRefreshPlan {
|
||||
plan: seed.build_get_follow_up_plan(&trace_id)?,
|
||||
projection_target: LocalVideoTaskProjectionTarget::OpenAi {
|
||||
task_id: seed.local_task_id.clone(),
|
||||
},
|
||||
}),
|
||||
LocalVideoTaskSnapshot::Gemini(seed) => Some(LocalVideoTaskReadRefreshPlan {
|
||||
plan: seed.build_get_follow_up_plan(&trace_id)?,
|
||||
projection_target: LocalVideoTaskProjectionTarget::Gemini {
|
||||
short_id: seed.local_short_id.clone(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn prepare_poll_refresh_plan_for_stored_task(
|
||||
&self,
|
||||
task: &StoredVideoTask,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskReadRefreshPlan> {
|
||||
if self.truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = LocalVideoTaskSnapshot::from_stored_task(task)?;
|
||||
match snapshot {
|
||||
LocalVideoTaskSnapshot::OpenAi(seed) => Some(LocalVideoTaskReadRefreshPlan {
|
||||
plan: seed.build_get_follow_up_plan(trace_id)?,
|
||||
projection_target: LocalVideoTaskProjectionTarget::OpenAi {
|
||||
task_id: seed.local_task_id.clone(),
|
||||
},
|
||||
}),
|
||||
LocalVideoTaskSnapshot::Gemini(seed) => Some(LocalVideoTaskReadRefreshPlan {
|
||||
plan: seed.build_get_follow_up_plan(trace_id)?,
|
||||
projection_target: LocalVideoTaskProjectionTarget::Gemini {
|
||||
short_id: seed.local_short_id.clone(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_read_refresh_projection(
|
||||
&self,
|
||||
refresh_plan: &LocalVideoTaskReadRefreshPlan,
|
||||
provider_body: &Map<String, Value>,
|
||||
) -> bool {
|
||||
match &refresh_plan.projection_target {
|
||||
LocalVideoTaskProjectionTarget::OpenAi { task_id } => {
|
||||
self.project_openai_task_response(task_id, provider_body)
|
||||
}
|
||||
LocalVideoTaskProjectionTarget::Gemini { short_id } => {
|
||||
self.project_gemini_task_response(short_id, provider_body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_follow_up_sync_plan(
|
||||
&self,
|
||||
plan_kind: &str,
|
||||
request_path: &str,
|
||||
body_json: Option<&Value>,
|
||||
fallback_user_id: Option<&str>,
|
||||
fallback_api_key_id: Option<&str>,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalVideoTaskFollowUpPlan> {
|
||||
match plan_kind {
|
||||
"openai_video_remix_sync" => {
|
||||
let task_id = extract_openai_task_id_from_remix_path(request_path)?;
|
||||
let seed = self.store.clone_openai(task_id)?;
|
||||
seed.build_remix_follow_up_plan(
|
||||
body_json?,
|
||||
fallback_user_id,
|
||||
fallback_api_key_id,
|
||||
trace_id,
|
||||
)
|
||||
}
|
||||
"openai_video_delete_sync" => {
|
||||
let task_id = extract_openai_task_id_from_path(request_path)?;
|
||||
let seed = self.store.clone_openai(task_id)?;
|
||||
seed.build_delete_follow_up_plan(fallback_user_id, fallback_api_key_id, trace_id)
|
||||
}
|
||||
"openai_video_cancel_sync" => {
|
||||
let task_id = extract_openai_task_id_from_cancel_path(request_path)?;
|
||||
let seed = self.store.clone_openai(task_id)?;
|
||||
seed.build_cancel_follow_up_plan(fallback_user_id, fallback_api_key_id, trace_id)
|
||||
}
|
||||
"gemini_video_cancel_sync" => {
|
||||
let short_id = extract_gemini_short_id_from_cancel_path(request_path)?;
|
||||
let seed = self.store.clone_gemini(short_id)?;
|
||||
seed.build_cancel_follow_up_plan(fallback_user_id, fallback_api_key_id, trace_id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
163
crates/aether-video-tasks-core/src/snapshot.rs
Normal file
163
crates/aether-video-tasks-core/src/snapshot.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use aether_data::repository::video_tasks::{StoredVideoTask, UpsertVideoTask};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
local_status_from_stored, non_empty_owned, request_body_string, GeminiVideoTaskSeed,
|
||||
LocalVideoTaskPersistence, LocalVideoTaskReadResponse, LocalVideoTaskSnapshot,
|
||||
LocalVideoTaskStatus, LocalVideoTaskTransport, OpenAiVideoTaskSeed,
|
||||
};
|
||||
|
||||
impl LocalVideoTaskSnapshot {
|
||||
pub fn to_upsert_record(&self) -> UpsertVideoTask {
|
||||
match self {
|
||||
Self::OpenAi(seed) => seed.to_upsert_record(),
|
||||
Self::Gemini(seed) => seed.to_upsert_record(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_stored_task(task: &StoredVideoTask) -> Option<Self> {
|
||||
task.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("rust_local_snapshot"))
|
||||
.cloned()
|
||||
.and_then(|value| serde_json::from_value::<LocalVideoTaskSnapshot>(value).ok())
|
||||
}
|
||||
|
||||
pub fn from_stored_task_with_transport(
|
||||
task: &StoredVideoTask,
|
||||
transport: LocalVideoTaskTransport,
|
||||
) -> Option<Self> {
|
||||
let provider_api_format = task.provider_api_format.as_deref()?.trim();
|
||||
let persistence = LocalVideoTaskPersistence::from_stored_task(task)?;
|
||||
|
||||
match provider_api_format {
|
||||
"openai:video" => {
|
||||
let upstream_task_id = non_empty_owned(task.external_task_id.as_ref())?;
|
||||
Some(Self::OpenAi(OpenAiVideoTaskSeed {
|
||||
local_task_id: task.id.clone(),
|
||||
upstream_task_id,
|
||||
created_at_unix_secs: task.created_at_unix_secs,
|
||||
user_id: task.user_id.clone(),
|
||||
api_key_id: task.api_key_id.clone(),
|
||||
model: non_empty_owned(task.model.as_ref()),
|
||||
prompt: non_empty_owned(task.prompt.as_ref()).or_else(|| {
|
||||
request_body_string(&persistence.original_request_body, "prompt")
|
||||
}),
|
||||
size: non_empty_owned(task.size.as_ref()).or_else(|| {
|
||||
request_body_string(&persistence.original_request_body, "size")
|
||||
}),
|
||||
seconds: task
|
||||
.duration_seconds
|
||||
.map(|value| value.to_string())
|
||||
.or_else(|| {
|
||||
request_body_string(&persistence.original_request_body, "seconds")
|
||||
}),
|
||||
remixed_from_video_id: request_body_string(
|
||||
&persistence.original_request_body,
|
||||
"remix_video_id",
|
||||
)
|
||||
.or_else(|| {
|
||||
request_body_string(
|
||||
&persistence.original_request_body,
|
||||
"remixed_from_video_id",
|
||||
)
|
||||
}),
|
||||
status: local_status_from_stored(task.status),
|
||||
progress_percent: task.progress_percent,
|
||||
completed_at_unix_secs: task.completed_at_unix_secs,
|
||||
expires_at_unix_secs: None,
|
||||
error_code: task.error_code.clone(),
|
||||
error_message: task.error_message.clone(),
|
||||
video_url: non_empty_owned(task.video_url.as_ref()),
|
||||
persistence,
|
||||
transport,
|
||||
}))
|
||||
}
|
||||
"gemini:video" => {
|
||||
let local_short_id =
|
||||
non_empty_owned(task.short_id.as_ref()).unwrap_or_else(|| task.id.clone());
|
||||
let upstream_operation_name = non_empty_owned(task.external_task_id.as_ref())?;
|
||||
let model = non_empty_owned(task.model.as_ref())?;
|
||||
Some(Self::Gemini(GeminiVideoTaskSeed {
|
||||
local_short_id,
|
||||
upstream_operation_name,
|
||||
user_id: task.user_id.clone(),
|
||||
api_key_id: task.api_key_id.clone(),
|
||||
model,
|
||||
status: local_status_from_stored(task.status),
|
||||
progress_percent: task.progress_percent,
|
||||
error_code: task.error_code.clone(),
|
||||
error_message: task.error_message.clone(),
|
||||
metadata: Value::Object(Map::new()),
|
||||
persistence,
|
||||
transport,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_response(&self) -> LocalVideoTaskReadResponse {
|
||||
match self {
|
||||
Self::OpenAi(seed) => match seed.status {
|
||||
LocalVideoTaskStatus::Cancelled => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task was cancelled"}),
|
||||
},
|
||||
LocalVideoTaskStatus::Deleted => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task not found"}),
|
||||
},
|
||||
_ => LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: seed.client_body_json(),
|
||||
},
|
||||
},
|
||||
Self::Gemini(seed) => match seed.status {
|
||||
LocalVideoTaskStatus::Cancelled => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task was cancelled"}),
|
||||
},
|
||||
LocalVideoTaskStatus::Deleted => LocalVideoTaskReadResponse {
|
||||
status_code: 404,
|
||||
body_json: json!({"detail": "Video task not found"}),
|
||||
},
|
||||
_ => LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: seed.client_body_json(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active_for_refresh(&self) -> bool {
|
||||
match self {
|
||||
Self::OpenAi(seed) => matches!(
|
||||
seed.status,
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing
|
||||
),
|
||||
Self::Gemini(seed) => matches!(
|
||||
seed.status,
|
||||
LocalVideoTaskStatus::Submitted
|
||||
| LocalVideoTaskStatus::Queued
|
||||
| LocalVideoTaskStatus::Processing
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_provider_body(&mut self, provider_body: &Map<String, Value>) {
|
||||
match self {
|
||||
Self::OpenAi(seed) => seed.apply_provider_body(provider_body),
|
||||
Self::Gemini(seed) => seed.apply_provider_body(provider_body),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::OpenAi(seed) => seed.transport.provider_name.as_deref(),
|
||||
Self::Gemini(seed) => seed.transport.provider_name.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
18
crates/aether-video-tasks-core/src/store.rs
Normal file
18
crates/aether-video-tasks-core/src/store.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{
|
||||
GeminiVideoTaskSeed, LocalVideoTaskReadResponse, LocalVideoTaskRegistryMutation,
|
||||
LocalVideoTaskSnapshot, OpenAiVideoTaskSeed,
|
||||
};
|
||||
|
||||
pub trait VideoTaskStore: std::fmt::Debug + Send + Sync {
|
||||
fn insert(&self, snapshot: LocalVideoTaskSnapshot);
|
||||
fn read_openai(&self, task_id: &str) -> Option<LocalVideoTaskReadResponse>;
|
||||
fn read_gemini(&self, short_id: &str) -> Option<LocalVideoTaskReadResponse>;
|
||||
fn clone_openai(&self, task_id: &str) -> Option<OpenAiVideoTaskSeed>;
|
||||
fn clone_gemini(&self, short_id: &str) -> Option<GeminiVideoTaskSeed>;
|
||||
fn list_active_snapshots(&self, limit: usize) -> Vec<LocalVideoTaskSnapshot>;
|
||||
fn apply_mutation(&self, mutation: LocalVideoTaskRegistryMutation);
|
||||
fn project_openai(&self, task_id: &str, provider_body: &Map<String, Value>) -> bool;
|
||||
fn project_gemini(&self, short_id: &str, provider_body: &Map<String, Value>) -> bool;
|
||||
}
|
||||
171
crates/aether-video-tasks-core/src/store_backend.rs
Normal file
171
crates/aether-video-tasks-core/src/store_backend.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{
|
||||
GeminiVideoTaskSeed, LocalVideoTaskReadResponse, LocalVideoTaskRegistryMutation,
|
||||
LocalVideoTaskSnapshot, OpenAiVideoTaskSeed, VideoTaskRegistry, VideoTaskStore,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InMemoryVideoTaskStore {
|
||||
registry: Mutex<VideoTaskRegistry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileVideoTaskStore {
|
||||
path: PathBuf,
|
||||
registry: Mutex<VideoTaskRegistry>,
|
||||
}
|
||||
|
||||
impl VideoTaskStore for InMemoryVideoTaskStore {
|
||||
fn insert(&self, snapshot: LocalVideoTaskSnapshot) {
|
||||
if let Ok(mut registry) = self.registry.lock() {
|
||||
registry.insert(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_openai(&self, task_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.read_openai(task_id)
|
||||
}
|
||||
|
||||
fn read_gemini(&self, short_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.read_gemini(short_id)
|
||||
}
|
||||
|
||||
fn clone_openai(&self, task_id: &str) -> Option<OpenAiVideoTaskSeed> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.clone_openai(task_id)
|
||||
}
|
||||
|
||||
fn clone_gemini(&self, short_id: &str) -> Option<GeminiVideoTaskSeed> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.clone_gemini(short_id)
|
||||
}
|
||||
|
||||
fn list_active_snapshots(&self, limit: usize) -> Vec<LocalVideoTaskSnapshot> {
|
||||
let Ok(registry) = self.registry.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
registry.list_active_snapshots(limit)
|
||||
}
|
||||
|
||||
fn apply_mutation(&self, mutation: LocalVideoTaskRegistryMutation) {
|
||||
if let Ok(mut registry) = self.registry.lock() {
|
||||
registry.apply_mutation(mutation);
|
||||
}
|
||||
}
|
||||
|
||||
fn project_openai(&self, task_id: &str, provider_body: &Map<String, Value>) -> bool {
|
||||
let Ok(mut registry) = self.registry.lock() else {
|
||||
return false;
|
||||
};
|
||||
registry.project_openai(task_id, provider_body)
|
||||
}
|
||||
|
||||
fn project_gemini(&self, short_id: &str, provider_body: &Map<String, Value>) -> bool {
|
||||
let Ok(mut registry) = self.registry.lock() else {
|
||||
return false;
|
||||
};
|
||||
registry.project_gemini(short_id, provider_body)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileVideoTaskStore {
|
||||
pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
|
||||
let path = path.into();
|
||||
let registry = Self::load_registry(&path)?;
|
||||
Ok(Self {
|
||||
path,
|
||||
registry: Mutex::new(registry),
|
||||
})
|
||||
}
|
||||
|
||||
fn load_registry(path: &Path) -> std::io::Result<VideoTaskRegistry> {
|
||||
if !path.exists() {
|
||||
return Ok(VideoTaskRegistry::default());
|
||||
}
|
||||
let bytes = std::fs::read(path)?;
|
||||
if bytes.is_empty() {
|
||||
return Ok(VideoTaskRegistry::default());
|
||||
}
|
||||
serde_json::from_slice(&bytes)
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
|
||||
}
|
||||
|
||||
fn persist_registry(&self, registry: &VideoTaskRegistry) -> std::io::Result<()> {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(registry)
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
|
||||
let temp_path = self.path.with_extension("tmp");
|
||||
std::fs::write(&temp_path, bytes)?;
|
||||
std::fs::rename(temp_path, &self.path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mutate_registry(&self, mutator: impl FnOnce(&mut VideoTaskRegistry) -> bool) -> bool {
|
||||
let Ok(mut registry) = self.registry.lock() else {
|
||||
return false;
|
||||
};
|
||||
if !mutator(&mut registry) {
|
||||
return false;
|
||||
}
|
||||
self.persist_registry(®istry).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoTaskStore for FileVideoTaskStore {
|
||||
fn insert(&self, snapshot: LocalVideoTaskSnapshot) {
|
||||
let _ = self.mutate_registry(|registry| {
|
||||
registry.insert(snapshot);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
fn read_openai(&self, task_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.read_openai(task_id)
|
||||
}
|
||||
|
||||
fn read_gemini(&self, short_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.read_gemini(short_id)
|
||||
}
|
||||
|
||||
fn clone_openai(&self, task_id: &str) -> Option<OpenAiVideoTaskSeed> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.clone_openai(task_id)
|
||||
}
|
||||
|
||||
fn clone_gemini(&self, short_id: &str) -> Option<GeminiVideoTaskSeed> {
|
||||
let registry = self.registry.lock().ok()?;
|
||||
registry.clone_gemini(short_id)
|
||||
}
|
||||
|
||||
fn list_active_snapshots(&self, limit: usize) -> Vec<LocalVideoTaskSnapshot> {
|
||||
let Ok(registry) = self.registry.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
registry.list_active_snapshots(limit)
|
||||
}
|
||||
|
||||
fn apply_mutation(&self, mutation: LocalVideoTaskRegistryMutation) {
|
||||
let _ = self.mutate_registry(|registry| {
|
||||
registry.apply_mutation(mutation);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
fn project_openai(&self, task_id: &str, provider_body: &Map<String, Value>) -> bool {
|
||||
self.mutate_registry(|registry| registry.project_openai(task_id, provider_body))
|
||||
}
|
||||
|
||||
fn project_gemini(&self, short_id: &str, provider_body: &Map<String, Value>) -> bool {
|
||||
self.mutate_registry(|registry| registry.project_gemini(short_id, provider_body))
|
||||
}
|
||||
}
|
||||
100
crates/aether-video-tasks-core/src/store_registry.rs
Normal file
100
crates/aether-video-tasks-core/src/store_registry.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{
|
||||
GeminiVideoTaskSeed, LocalVideoTaskReadResponse, LocalVideoTaskRegistryMutation,
|
||||
LocalVideoTaskSnapshot, LocalVideoTaskStatus, OpenAiVideoTaskSeed,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct VideoTaskRegistry {
|
||||
openai: BTreeMap<String, LocalVideoTaskSnapshot>,
|
||||
gemini: BTreeMap<String, LocalVideoTaskSnapshot>,
|
||||
}
|
||||
|
||||
impl VideoTaskRegistry {
|
||||
pub fn insert(&mut self, snapshot: LocalVideoTaskSnapshot) {
|
||||
match &snapshot {
|
||||
LocalVideoTaskSnapshot::OpenAi(seed) => {
|
||||
self.openai.insert(seed.local_task_id.clone(), snapshot);
|
||||
}
|
||||
LocalVideoTaskSnapshot::Gemini(seed) => {
|
||||
self.gemini.insert(seed.local_short_id.clone(), snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_openai(&self, task_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
||||
self.openai
|
||||
.get(task_id)
|
||||
.map(LocalVideoTaskSnapshot::read_response)
|
||||
}
|
||||
|
||||
pub fn read_gemini(&self, short_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
||||
self.gemini
|
||||
.get(short_id)
|
||||
.map(LocalVideoTaskSnapshot::read_response)
|
||||
}
|
||||
|
||||
pub fn clone_openai(&self, task_id: &str) -> Option<OpenAiVideoTaskSeed> {
|
||||
let LocalVideoTaskSnapshot::OpenAi(seed) = self.openai.get(task_id)?.clone() else {
|
||||
return None;
|
||||
};
|
||||
Some(seed)
|
||||
}
|
||||
|
||||
pub fn clone_gemini(&self, short_id: &str) -> Option<GeminiVideoTaskSeed> {
|
||||
let LocalVideoTaskSnapshot::Gemini(seed) = self.gemini.get(short_id)?.clone() else {
|
||||
return None;
|
||||
};
|
||||
Some(seed)
|
||||
}
|
||||
|
||||
pub fn list_active_snapshots(&self, limit: usize) -> Vec<LocalVideoTaskSnapshot> {
|
||||
self.openai
|
||||
.values()
|
||||
.chain(self.gemini.values())
|
||||
.filter(|snapshot| snapshot.is_active_for_refresh())
|
||||
.take(limit)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn apply_mutation(&mut self, mutation: LocalVideoTaskRegistryMutation) {
|
||||
match mutation {
|
||||
LocalVideoTaskRegistryMutation::OpenAiCancelled { task_id } => {
|
||||
if let Some(LocalVideoTaskSnapshot::OpenAi(seed)) = self.openai.get_mut(&task_id) {
|
||||
seed.status = LocalVideoTaskStatus::Cancelled;
|
||||
}
|
||||
}
|
||||
LocalVideoTaskRegistryMutation::OpenAiDeleted { task_id } => {
|
||||
if let Some(LocalVideoTaskSnapshot::OpenAi(seed)) = self.openai.get_mut(&task_id) {
|
||||
seed.status = LocalVideoTaskStatus::Deleted;
|
||||
}
|
||||
}
|
||||
LocalVideoTaskRegistryMutation::GeminiCancelled { short_id } => {
|
||||
if let Some(LocalVideoTaskSnapshot::Gemini(seed)) = self.gemini.get_mut(&short_id) {
|
||||
seed.status = LocalVideoTaskStatus::Cancelled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_openai(&mut self, task_id: &str, provider_body: &Map<String, Value>) -> bool {
|
||||
let Some(LocalVideoTaskSnapshot::OpenAi(seed)) = self.openai.get_mut(task_id) else {
|
||||
return false;
|
||||
};
|
||||
seed.apply_provider_body(provider_body);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn project_gemini(&mut self, short_id: &str, provider_body: &Map<String, Value>) -> bool {
|
||||
let Some(LocalVideoTaskSnapshot::Gemini(seed)) = self.gemini.get_mut(short_id) else {
|
||||
return false;
|
||||
};
|
||||
seed.apply_provider_body(provider_body);
|
||||
true
|
||||
}
|
||||
}
|
||||
473
crates/aether-video-tasks-core/src/sync.rs
Normal file
473
crates/aether-video-tasks-core/src/sync.rs
Normal file
@@ -0,0 +1,473 @@
|
||||
use aether_contracts::ExecutionPlan;
|
||||
use serde_json::{json, Map, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
context_text, context_u64, current_unix_timestamp_secs, generate_local_short_id,
|
||||
request_body_text, GeminiVideoTaskSeed, LocalVideoTaskPersistence, LocalVideoTaskReadResponse,
|
||||
LocalVideoTaskSeed, LocalVideoTaskSnapshot, LocalVideoTaskStatus, LocalVideoTaskSuccessPlan,
|
||||
LocalVideoTaskTransport, OpenAiVideoTaskSeed, VideoTaskSyncReportMode,
|
||||
VideoTaskTruthSourceMode,
|
||||
};
|
||||
|
||||
impl LocalVideoTaskSeed {
|
||||
pub fn from_sync_finalize(
|
||||
report_kind: &str,
|
||||
provider_body: &Map<String, Value>,
|
||||
report_context: &Map<String, Value>,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Option<Self> {
|
||||
let transport = LocalVideoTaskTransport::from_plan(plan)?;
|
||||
let persistence = LocalVideoTaskPersistence::from_report_context(report_context, plan);
|
||||
match report_kind {
|
||||
"openai_video_create_sync_finalize" => {
|
||||
let upstream_id = provider_body.get("id").and_then(Value::as_str)?.trim();
|
||||
if upstream_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self::OpenAiCreate(OpenAiVideoTaskSeed {
|
||||
local_task_id: context_text(report_context, "local_task_id")
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string()),
|
||||
upstream_task_id: upstream_id.to_string(),
|
||||
created_at_unix_secs: context_u64(report_context, "local_created_at")
|
||||
.unwrap_or_else(current_unix_timestamp_secs),
|
||||
user_id: context_text(report_context, "user_id"),
|
||||
api_key_id: context_text(report_context, "api_key_id"),
|
||||
model: context_text(report_context, "model")
|
||||
.or_else(|| request_body_text(report_context, "model")),
|
||||
prompt: request_body_text(report_context, "prompt"),
|
||||
size: request_body_text(report_context, "size"),
|
||||
seconds: request_body_text(report_context, "seconds"),
|
||||
remixed_from_video_id: None,
|
||||
status: LocalVideoTaskStatus::Submitted,
|
||||
progress_percent: 0,
|
||||
completed_at_unix_secs: None,
|
||||
expires_at_unix_secs: None,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
video_url: None,
|
||||
persistence: persistence.clone(),
|
||||
transport: transport.clone(),
|
||||
}))
|
||||
}
|
||||
"openai_video_remix_sync_finalize" => {
|
||||
let upstream_id = provider_body.get("id").and_then(Value::as_str)?.trim();
|
||||
if upstream_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self::OpenAiRemix(OpenAiVideoTaskSeed {
|
||||
local_task_id: context_text(report_context, "local_task_id")
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string()),
|
||||
upstream_task_id: upstream_id.to_string(),
|
||||
created_at_unix_secs: context_u64(report_context, "local_created_at")
|
||||
.unwrap_or_else(current_unix_timestamp_secs),
|
||||
user_id: context_text(report_context, "user_id"),
|
||||
api_key_id: context_text(report_context, "api_key_id"),
|
||||
model: context_text(report_context, "model")
|
||||
.or_else(|| request_body_text(report_context, "model")),
|
||||
prompt: request_body_text(report_context, "prompt"),
|
||||
size: request_body_text(report_context, "size"),
|
||||
seconds: request_body_text(report_context, "seconds"),
|
||||
remixed_from_video_id: context_text(report_context, "task_id")
|
||||
.or_else(|| request_body_text(report_context, "remix_video_id")),
|
||||
status: LocalVideoTaskStatus::Submitted,
|
||||
progress_percent: 0,
|
||||
completed_at_unix_secs: None,
|
||||
expires_at_unix_secs: None,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
video_url: None,
|
||||
persistence: persistence.clone(),
|
||||
transport: transport.clone(),
|
||||
}))
|
||||
}
|
||||
"gemini_video_create_sync_finalize" => {
|
||||
let operation_name = provider_body
|
||||
.get("name")
|
||||
.or_else(|| provider_body.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
|
||||
Some(Self::GeminiCreate(GeminiVideoTaskSeed {
|
||||
local_short_id: context_text(report_context, "local_short_id")
|
||||
.unwrap_or_else(generate_local_short_id),
|
||||
upstream_operation_name: operation_name.to_string(),
|
||||
user_id: context_text(report_context, "user_id"),
|
||||
api_key_id: context_text(report_context, "api_key_id"),
|
||||
model: context_text(report_context, "model")
|
||||
.or_else(|| context_text(report_context, "model_name"))
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
status: LocalVideoTaskStatus::Submitted,
|
||||
progress_percent: 0,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
metadata: json!({}),
|
||||
persistence,
|
||||
transport,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success_report_kind(&self) -> &'static str {
|
||||
match self {
|
||||
Self::OpenAiCreate(_) => "openai_video_create_sync_success",
|
||||
Self::OpenAiRemix(_) => "openai_video_remix_sync_success",
|
||||
Self::GeminiCreate(_) => "gemini_video_create_sync_success",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_report_context(&self, report_context: &mut Map<String, Value>) {
|
||||
match self {
|
||||
Self::OpenAiCreate(seed) | Self::OpenAiRemix(seed) => {
|
||||
report_context.insert(
|
||||
"local_task_id".to_string(),
|
||||
Value::String(seed.local_task_id.clone()),
|
||||
);
|
||||
report_context.insert(
|
||||
"local_created_at".to_string(),
|
||||
Value::Number(seed.created_at_unix_secs.into()),
|
||||
);
|
||||
}
|
||||
Self::GeminiCreate(seed) => {
|
||||
report_context.insert(
|
||||
"local_short_id".to_string(),
|
||||
Value::String(seed.local_short_id.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_body_json(&self) -> Value {
|
||||
match self {
|
||||
Self::OpenAiCreate(seed) | Self::OpenAiRemix(seed) => seed.client_body_json(),
|
||||
Self::GeminiCreate(seed) => seed.client_body_json(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoTaskTruthSourceMode {
|
||||
pub fn prepare_sync_success(
|
||||
self,
|
||||
report_kind: &str,
|
||||
provider_body: &Map<String, Value>,
|
||||
report_context: &Map<String, Value>,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Option<LocalVideoTaskSuccessPlan> {
|
||||
let seed = LocalVideoTaskSeed::from_sync_finalize(
|
||||
report_kind,
|
||||
provider_body,
|
||||
report_context,
|
||||
plan,
|
||||
)?;
|
||||
let report_mode = match self {
|
||||
Self::PythonSyncReport => VideoTaskSyncReportMode::InlineSync,
|
||||
Self::RustAuthoritative => VideoTaskSyncReportMode::Background,
|
||||
};
|
||||
Some(LocalVideoTaskSuccessPlan { seed, report_mode })
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalVideoTaskSuccessPlan {
|
||||
pub fn success_report_kind(&self) -> &'static str {
|
||||
self.seed.success_report_kind()
|
||||
}
|
||||
|
||||
pub fn report_mode(&self) -> VideoTaskSyncReportMode {
|
||||
self.report_mode
|
||||
}
|
||||
|
||||
pub fn apply_to_report_context(&self, report_context: &mut Map<String, Value>) {
|
||||
self.seed.apply_to_report_context(report_context);
|
||||
if matches!(self.report_mode, VideoTaskSyncReportMode::Background) {
|
||||
report_context.insert("rust_video_task_persisted".to_string(), Value::Bool(true));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_body_json(&self) -> Value {
|
||||
self.seed.client_body_json()
|
||||
}
|
||||
|
||||
pub fn to_snapshot(&self) -> LocalVideoTaskSnapshot {
|
||||
match &self.seed {
|
||||
LocalVideoTaskSeed::OpenAiCreate(seed) | LocalVideoTaskSeed::OpenAiRemix(seed) => {
|
||||
LocalVideoTaskSnapshot::OpenAi(seed.clone())
|
||||
}
|
||||
LocalVideoTaskSeed::GeminiCreate(seed) => LocalVideoTaskSnapshot::Gemini(seed.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_internal_finalize_video_plan(
|
||||
trace_id: &str,
|
||||
signature: &str,
|
||||
report_context: Option<&Value>,
|
||||
) -> Option<ExecutionPlan> {
|
||||
if !matches!(signature, "openai:video" | "gemini:video") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let report_context = report_context.and_then(Value::as_object);
|
||||
let context_text = |key: &str| {
|
||||
report_context
|
||||
.and_then(|value| value.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
};
|
||||
|
||||
let provider_name = signature
|
||||
.split(':')
|
||||
.next()
|
||||
.expect("video finalize signature should include provider name")
|
||||
.to_string();
|
||||
let model_name = context_text("model")
|
||||
.or_else(|| context_text("model_name"))
|
||||
.or_else(|| match signature {
|
||||
"openai:video" => Some("sora-2".to_string()),
|
||||
"gemini:video" => Some("veo-3".to_string()),
|
||||
_ => None,
|
||||
});
|
||||
let original_request_body = report_context
|
||||
.and_then(|value| value.get("original_request_body"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let url = match signature {
|
||||
"openai:video" => "https://internal.gateway.invalid/v1/videos".to_string(),
|
||||
"gemini:video" => format!(
|
||||
"https://internal.gateway.invalid/v1beta/models/{}:predictLongRunning",
|
||||
model_name.clone().unwrap_or_else(|| "veo-3".to_string())
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(ExecutionPlan {
|
||||
request_id: context_text("request_id").unwrap_or_else(|| trace_id.to_string()),
|
||||
candidate_id: None,
|
||||
provider_name: Some(provider_name.clone()),
|
||||
provider_id: context_text("provider_id")
|
||||
.unwrap_or_else(|| format!("internal-{provider_name}-video-provider")),
|
||||
endpoint_id: context_text("endpoint_id")
|
||||
.unwrap_or_else(|| format!("internal-{provider_name}-video-endpoint")),
|
||||
key_id: context_text("key_id")
|
||||
.or_else(|| context_text("api_key_id"))
|
||||
.unwrap_or_else(|| format!("internal-{provider_name}-video-key")),
|
||||
method: "POST".to_string(),
|
||||
url,
|
||||
headers: std::collections::BTreeMap::from([(
|
||||
"authorization".to_string(),
|
||||
"Bearer internal-gateway".to_string(),
|
||||
)]),
|
||||
content_type: Some("application/json".to_string()),
|
||||
content_encoding: None,
|
||||
body: aether_contracts::RequestBody::from_json(original_request_body),
|
||||
stream: false,
|
||||
client_api_format: signature.to_string(),
|
||||
provider_api_format: signature.to_string(),
|
||||
model_name,
|
||||
proxy: Some(aether_contracts::ProxySnapshot {
|
||||
enabled: Some(false),
|
||||
mode: Some("direct".to_string()),
|
||||
node_id: None,
|
||||
label: None,
|
||||
url: None,
|
||||
extra: None,
|
||||
}),
|
||||
tls_profile: None,
|
||||
timeouts: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_local_sync_finalize_read_response(
|
||||
report_kind: &str,
|
||||
upstream_status_code: u16,
|
||||
report_context: Option<&Value>,
|
||||
) -> Option<LocalVideoTaskReadResponse> {
|
||||
match report_kind {
|
||||
"openai_video_delete_sync_finalize" => {
|
||||
if upstream_status_code >= 400 && upstream_status_code != 404 {
|
||||
return None;
|
||||
}
|
||||
let task_id = report_context
|
||||
.and_then(|value| value.get("task_id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
Some(LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: json!({
|
||||
"id": task_id,
|
||||
"object": "video",
|
||||
"deleted": true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
"openai_video_cancel_sync_finalize" | "gemini_video_cancel_sync_finalize" => {
|
||||
if upstream_status_code >= 400 {
|
||||
return None;
|
||||
}
|
||||
Some(LocalVideoTaskReadResponse {
|
||||
status_code: 200,
|
||||
body_json: json!({}),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_local_sync_success_background_report_kind(
|
||||
report_kind: &str,
|
||||
) -> Option<&'static str> {
|
||||
match report_kind {
|
||||
"openai_video_delete_sync_finalize" => Some("openai_video_delete_sync_success"),
|
||||
"openai_video_cancel_sync_finalize" => Some("openai_video_cancel_sync_success"),
|
||||
"gemini_video_cancel_sync_finalize" => Some("gemini_video_cancel_sync_success"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_local_sync_error_background_report_kind(report_kind: &str) -> Option<&'static str> {
|
||||
match report_kind {
|
||||
"openai_video_create_sync_finalize" => Some("openai_video_create_sync_error"),
|
||||
"openai_video_remix_sync_finalize" => Some("openai_video_remix_sync_error"),
|
||||
"gemini_video_create_sync_finalize" => Some("gemini_video_create_sync_error"),
|
||||
"openai_video_delete_sync_finalize" => Some("openai_video_delete_sync_error"),
|
||||
"openai_video_cancel_sync_finalize" => Some("openai_video_cancel_sync_error"),
|
||||
"gemini_video_cancel_sync_finalize" => Some("gemini_video_cancel_sync_error"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_internal_finalize_video_plan, build_local_sync_finalize_read_response,
|
||||
resolve_local_sync_error_background_report_kind,
|
||||
resolve_local_sync_success_background_report_kind,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn builds_local_sync_finalize_read_response_for_supported_video_finalize_kinds() {
|
||||
let delete_response = build_local_sync_finalize_read_response(
|
||||
"openai_video_delete_sync_finalize",
|
||||
404,
|
||||
Some(&json!({"task_id": "task-123"})),
|
||||
)
|
||||
.expect("delete finalize response should build");
|
||||
assert_eq!(delete_response.status_code, 200);
|
||||
assert_eq!(
|
||||
delete_response.body_json,
|
||||
json!({
|
||||
"id": "task-123",
|
||||
"object": "video",
|
||||
"deleted": true,
|
||||
})
|
||||
);
|
||||
|
||||
let cancel_response = build_local_sync_finalize_read_response(
|
||||
"gemini_video_cancel_sync_finalize",
|
||||
200,
|
||||
Some(&json!({})),
|
||||
)
|
||||
.expect("cancel finalize response should build");
|
||||
assert_eq!(cancel_response.status_code, 200);
|
||||
assert_eq!(cancel_response.body_json, json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_local_sync_finalize_read_response_when_status_or_context_is_invalid() {
|
||||
assert!(build_local_sync_finalize_read_response(
|
||||
"openai_video_delete_sync_finalize",
|
||||
500,
|
||||
Some(&json!({"task_id": "task-123"})),
|
||||
)
|
||||
.is_none());
|
||||
assert!(build_local_sync_finalize_read_response(
|
||||
"openai_video_delete_sync_finalize",
|
||||
200,
|
||||
Some(&json!({})),
|
||||
)
|
||||
.is_none());
|
||||
assert!(build_local_sync_finalize_read_response(
|
||||
"openai_video_cancel_sync_finalize",
|
||||
500,
|
||||
Some(&json!({})),
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_local_sync_success_background_report_kind_for_supported_video_finalize_kinds() {
|
||||
assert_eq!(
|
||||
resolve_local_sync_success_background_report_kind("openai_video_delete_sync_finalize"),
|
||||
Some("openai_video_delete_sync_success")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_local_sync_success_background_report_kind("openai_video_create_sync_finalize"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_local_sync_error_background_report_kind_for_supported_video_finalize_kinds() {
|
||||
assert_eq!(
|
||||
resolve_local_sync_error_background_report_kind("openai_video_remix_sync_finalize"),
|
||||
Some("openai_video_remix_sync_error")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_local_sync_error_background_report_kind("unknown_finalize_kind"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_internal_finalize_video_plan_for_supported_video_signatures() {
|
||||
let openai_plan = build_internal_finalize_video_plan(
|
||||
"trace-openai-video",
|
||||
"openai:video",
|
||||
Some(&json!({
|
||||
"request_id": "req-openai-video",
|
||||
"model": "sora-2",
|
||||
"api_key_id": "api-key-openai-video",
|
||||
"original_request_body": { "prompt": "make a trailer" }
|
||||
})),
|
||||
)
|
||||
.expect("openai video plan should build");
|
||||
assert_eq!(openai_plan.request_id, "req-openai-video");
|
||||
assert_eq!(
|
||||
openai_plan.url,
|
||||
"https://internal.gateway.invalid/v1/videos"
|
||||
);
|
||||
assert_eq!(openai_plan.model_name.as_deref(), Some("sora-2"));
|
||||
|
||||
let gemini_plan = build_internal_finalize_video_plan(
|
||||
"trace-gemini-video",
|
||||
"gemini:video",
|
||||
Some(&json!({
|
||||
"local_short_id": "short-123"
|
||||
})),
|
||||
)
|
||||
.expect("gemini video plan should build");
|
||||
assert_eq!(
|
||||
gemini_plan.url,
|
||||
"https://internal.gateway.invalid/v1beta/models/veo-3:predictLongRunning"
|
||||
);
|
||||
assert_eq!(gemini_plan.model_name.as_deref(), Some("veo-3"));
|
||||
assert_eq!(gemini_plan.request_id, "trace-gemini-video");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_internal_finalize_video_plan_for_non_video_signatures() {
|
||||
assert!(
|
||||
build_internal_finalize_video_plan("trace-123", "openai:chat", Some(&json!({})))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
106
crates/aether-video-tasks-core/src/transport.rs
Normal file
106
crates/aether-video-tasks-core/src/transport.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::LocalVideoTaskStatus;
|
||||
|
||||
pub fn parse_video_content_variant(query_string: Option<&str>) -> Option<&'static str> {
|
||||
let mut variant = "video";
|
||||
if let Some(query_string) = query_string {
|
||||
for (key, value) in url::form_urlencoded::parse(query_string.as_bytes()) {
|
||||
if key == "variant" {
|
||||
variant = match value.as_ref() {
|
||||
"video" => "video",
|
||||
"thumbnail" => "thumbnail",
|
||||
"spritesheet" => "spritesheet",
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(variant)
|
||||
}
|
||||
|
||||
pub fn gemini_metadata_video_url(metadata: &Value) -> Option<String> {
|
||||
metadata
|
||||
.get("response")
|
||||
.and_then(|value| value.get("generateVideoResponse"))
|
||||
.and_then(|value| value.get("generatedSamples"))
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|value| value.first())
|
||||
.and_then(|value| value.get("video"))
|
||||
.and_then(|value| value.get("uri"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub fn map_openai_task_status(status: LocalVideoTaskStatus) -> &'static str {
|
||||
match status {
|
||||
LocalVideoTaskStatus::Submitted | LocalVideoTaskStatus::Queued => "queued",
|
||||
LocalVideoTaskStatus::Processing => "processing",
|
||||
LocalVideoTaskStatus::Completed => "completed",
|
||||
LocalVideoTaskStatus::Failed
|
||||
| LocalVideoTaskStatus::Cancelled
|
||||
| LocalVideoTaskStatus::Expired => "failed",
|
||||
LocalVideoTaskStatus::Deleted => "deleted",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::LocalVideoTaskStatus;
|
||||
|
||||
use super::{gemini_metadata_video_url, map_openai_task_status, parse_video_content_variant};
|
||||
|
||||
#[test]
|
||||
fn parses_supported_video_content_variants() {
|
||||
assert_eq!(parse_video_content_variant(None), Some("video"));
|
||||
assert_eq!(
|
||||
parse_video_content_variant(Some("variant=thumbnail")),
|
||||
Some("thumbnail")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_video_content_variant(Some("variant=spritesheet")),
|
||||
Some("spritesheet")
|
||||
);
|
||||
assert_eq!(parse_video_content_variant(Some("variant=invalid")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_gemini_metadata_video_url() {
|
||||
let metadata = json!({
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": "https://example.com/video.mp4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
gemini_metadata_video_url(&metadata).as_deref(),
|
||||
Some("https://example.com/video.mp4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_openai_task_status() {
|
||||
assert_eq!(
|
||||
map_openai_task_status(LocalVideoTaskStatus::Queued),
|
||||
"queued"
|
||||
);
|
||||
assert_eq!(
|
||||
map_openai_task_status(LocalVideoTaskStatus::Completed),
|
||||
"completed"
|
||||
);
|
||||
assert_eq!(
|
||||
map_openai_task_status(LocalVideoTaskStatus::Failed),
|
||||
"failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
114
crates/aether-video-tasks-core/src/transport_domain.rs
Normal file
114
crates/aether-video-tasks-core/src/transport_domain.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use aether_contracts::ExecutionPlan;
|
||||
use aether_data::repository::video_tasks::{
|
||||
StoredVideoTask, VideoTaskStatus as StoredVideoTaskStatus,
|
||||
};
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
context_text, non_empty_owned, LocalVideoTaskPersistence, LocalVideoTaskStatus,
|
||||
LocalVideoTaskTransport, LocalVideoTaskTransportBridgeInput,
|
||||
};
|
||||
|
||||
impl LocalVideoTaskTransport {
|
||||
pub fn from_plan(plan: &ExecutionPlan) -> Option<Self> {
|
||||
let upstream_base_url = match plan.provider_api_format.as_str() {
|
||||
"openai:video" => plan.url.split("/v1/videos").next()?.to_string(),
|
||||
"gemini:video" => plan.url.split("/v1beta/").next()?.to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
if upstream_base_url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
upstream_base_url,
|
||||
provider_name: plan.provider_name.clone(),
|
||||
provider_id: plan.provider_id.clone(),
|
||||
endpoint_id: plan.endpoint_id.clone(),
|
||||
key_id: plan.key_id.clone(),
|
||||
headers: plan.headers.clone(),
|
||||
content_type: plan.content_type.clone(),
|
||||
model_name: plan.model_name.clone(),
|
||||
proxy: plan.proxy.clone(),
|
||||
tls_profile: plan.tls_profile.clone(),
|
||||
timeouts: plan.timeouts.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_bridge_input(input: LocalVideoTaskTransportBridgeInput) -> Self {
|
||||
let mut headers = BTreeMap::new();
|
||||
headers.insert(input.auth_header, input.auth_value);
|
||||
|
||||
Self {
|
||||
upstream_base_url: input.upstream_base_url,
|
||||
provider_name: input.provider_name,
|
||||
provider_id: input.provider_id,
|
||||
endpoint_id: input.endpoint_id,
|
||||
key_id: input.key_id,
|
||||
headers,
|
||||
content_type: input.content_type,
|
||||
model_name: input.model_name,
|
||||
proxy: input.proxy,
|
||||
tls_profile: input.tls_profile,
|
||||
timeouts: input.timeouts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalVideoTaskPersistence {
|
||||
pub fn from_report_context(report_context: &Map<String, Value>, plan: &ExecutionPlan) -> Self {
|
||||
Self {
|
||||
request_id: context_text(report_context, "request_id")
|
||||
.unwrap_or_else(|| plan.request_id.clone()),
|
||||
username: context_text(report_context, "username"),
|
||||
api_key_name: context_text(report_context, "api_key_name"),
|
||||
client_api_format: context_text(report_context, "client_api_format")
|
||||
.unwrap_or_else(|| plan.client_api_format.clone()),
|
||||
provider_api_format: context_text(report_context, "provider_api_format")
|
||||
.unwrap_or_else(|| plan.provider_api_format.clone()),
|
||||
original_request_body: report_context
|
||||
.get("original_request_body")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())),
|
||||
format_converted: report_context
|
||||
.get("format_converted")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_stored_task(task: &StoredVideoTask) -> Option<Self> {
|
||||
let client_api_format = non_empty_owned(task.client_api_format.as_ref())
|
||||
.or_else(|| non_empty_owned(task.provider_api_format.as_ref()))?;
|
||||
let provider_api_format = non_empty_owned(task.provider_api_format.as_ref())
|
||||
.or_else(|| non_empty_owned(task.client_api_format.as_ref()))?;
|
||||
|
||||
Some(Self {
|
||||
request_id: task.request_id.clone(),
|
||||
username: task.username.clone(),
|
||||
api_key_name: task.api_key_name.clone(),
|
||||
client_api_format,
|
||||
provider_api_format,
|
||||
original_request_body: task
|
||||
.original_request_body
|
||||
.clone()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())),
|
||||
format_converted: task.format_converted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalVideoTaskStatus {
|
||||
pub fn as_database_status(self) -> StoredVideoTaskStatus {
|
||||
match self {
|
||||
Self::Submitted => StoredVideoTaskStatus::Submitted,
|
||||
Self::Queued => StoredVideoTaskStatus::Queued,
|
||||
Self::Processing => StoredVideoTaskStatus::Processing,
|
||||
Self::Completed => StoredVideoTaskStatus::Completed,
|
||||
Self::Failed => StoredVideoTaskStatus::Failed,
|
||||
Self::Cancelled => StoredVideoTaskStatus::Cancelled,
|
||||
Self::Expired => StoredVideoTaskStatus::Expired,
|
||||
Self::Deleted => StoredVideoTaskStatus::Deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
171
crates/aether-video-tasks-core/src/types.rs
Normal file
171
crates/aether-video-tasks-core/src/types.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aether_contracts::{ExecutionPlan, ExecutionTimeouts, ProxySnapshot};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
pub const DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS: u32 = 10;
|
||||
pub const DEFAULT_VIDEO_TASK_MAX_POLL_COUNT: u32 = 360;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VideoTaskSyncReportMode {
|
||||
InlineSync,
|
||||
Background,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum VideoTaskTruthSourceMode {
|
||||
#[default]
|
||||
PythonSyncReport,
|
||||
RustAuthoritative,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalVideoTaskSuccessPlan {
|
||||
pub seed: LocalVideoTaskSeed,
|
||||
pub report_mode: VideoTaskSyncReportMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalVideoTaskFollowUpPlan {
|
||||
pub plan: ExecutionPlan,
|
||||
pub report_kind: Option<String>,
|
||||
pub report_context: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalVideoTaskReadRefreshPlan {
|
||||
pub plan: ExecutionPlan,
|
||||
pub projection_target: LocalVideoTaskProjectionTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LocalVideoTaskContentAction {
|
||||
Immediate { status_code: u16, body_json: Value },
|
||||
StreamPlan(ExecutionPlan),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LocalVideoTaskProjectionTarget {
|
||||
OpenAi { task_id: String },
|
||||
Gemini { short_id: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum LocalVideoTaskSnapshot {
|
||||
OpenAi(OpenAiVideoTaskSeed),
|
||||
Gemini(GeminiVideoTaskSeed),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum LocalVideoTaskStatus {
|
||||
Submitted,
|
||||
Queued,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
Expired,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LocalVideoTaskReadResponse {
|
||||
pub status_code: u16,
|
||||
pub body_json: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LocalVideoTaskRegistryMutation {
|
||||
OpenAiCancelled { task_id: String },
|
||||
OpenAiDeleted { task_id: String },
|
||||
GeminiCancelled { short_id: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LocalVideoTaskSeed {
|
||||
OpenAiCreate(OpenAiVideoTaskSeed),
|
||||
OpenAiRemix(OpenAiVideoTaskSeed),
|
||||
GeminiCreate(GeminiVideoTaskSeed),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LocalVideoTaskTransport {
|
||||
pub upstream_base_url: String,
|
||||
pub provider_name: Option<String>,
|
||||
pub provider_id: String,
|
||||
pub endpoint_id: String,
|
||||
pub key_id: String,
|
||||
pub headers: BTreeMap<String, String>,
|
||||
pub content_type: Option<String>,
|
||||
pub model_name: Option<String>,
|
||||
pub proxy: Option<ProxySnapshot>,
|
||||
pub tls_profile: Option<String>,
|
||||
pub timeouts: Option<ExecutionTimeouts>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalVideoTaskTransportBridgeInput {
|
||||
pub upstream_base_url: String,
|
||||
pub provider_name: Option<String>,
|
||||
pub provider_id: String,
|
||||
pub endpoint_id: String,
|
||||
pub key_id: String,
|
||||
pub auth_header: String,
|
||||
pub auth_value: String,
|
||||
pub content_type: Option<String>,
|
||||
pub model_name: Option<String>,
|
||||
pub proxy: Option<ProxySnapshot>,
|
||||
pub tls_profile: Option<String>,
|
||||
pub timeouts: Option<ExecutionTimeouts>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LocalVideoTaskPersistence {
|
||||
pub request_id: String,
|
||||
pub username: Option<String>,
|
||||
pub api_key_name: Option<String>,
|
||||
pub client_api_format: String,
|
||||
pub provider_api_format: String,
|
||||
pub original_request_body: Value,
|
||||
pub format_converted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OpenAiVideoTaskSeed {
|
||||
pub local_task_id: String,
|
||||
pub upstream_task_id: String,
|
||||
pub created_at_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
pub api_key_id: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub prompt: Option<String>,
|
||||
pub size: Option<String>,
|
||||
pub seconds: Option<String>,
|
||||
pub remixed_from_video_id: Option<String>,
|
||||
pub status: LocalVideoTaskStatus,
|
||||
pub progress_percent: u16,
|
||||
pub completed_at_unix_secs: Option<u64>,
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub error_code: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
pub video_url: Option<String>,
|
||||
pub persistence: LocalVideoTaskPersistence,
|
||||
pub transport: LocalVideoTaskTransport,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GeminiVideoTaskSeed {
|
||||
pub local_short_id: String,
|
||||
pub upstream_operation_name: String,
|
||||
pub user_id: Option<String>,
|
||||
pub api_key_id: Option<String>,
|
||||
pub model: String,
|
||||
pub status: LocalVideoTaskStatus,
|
||||
pub progress_percent: u16,
|
||||
pub error_code: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
pub metadata: Value,
|
||||
pub persistence: LocalVideoTaskPersistence,
|
||||
pub transport: LocalVideoTaskTransport,
|
||||
}
|
||||
21
crates/aether-video-tasks-core/src/util.rs
Normal file
21
crates/aether-video-tasks-core/src/util.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub fn non_empty_owned(value: Option<&String>) -> Option<String> {
|
||||
value
|
||||
.map(String::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::non_empty_owned;
|
||||
|
||||
#[test]
|
||||
fn discards_blank_strings() {
|
||||
let empty = String::from(" ");
|
||||
let value = String::from(" hello ");
|
||||
|
||||
assert_eq!(non_empty_owned(Some(&empty)), None);
|
||||
assert_eq!(non_empty_owned(Some(&value)).as_deref(), Some("hello"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user