2026-05-02 13:23:54 +08:00
|
|
|
use std::collections::BTreeMap;
|
2026-09-04 03:45:52 +08:00
|
|
|
use std::fmt;
|
2026-05-02 13:23:54 +08:00
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
use aether_contracts::ProxySnapshot;
|
2026-04-07 02:50:19 +08:00
|
|
|
use aether_data_contracts::repository::video_tasks::StoredVideoTask;
|
2026-04-05 20:23:16 +08:00
|
|
|
use aether_video_tasks_core::{
|
|
|
|
|
LocalVideoTaskSnapshot, LocalVideoTaskTransport, LocalVideoTaskTransportBridgeInput,
|
|
|
|
|
};
|
|
|
|
|
use async_trait::async_trait;
|
2026-05-02 13:23:54 +08:00
|
|
|
use serde_json::Value;
|
2026-04-05 20:23:16 +08:00
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
use super::auth::{
|
|
|
|
|
build_passthrough_headers_with_auth, resolve_local_gemini_auth,
|
|
|
|
|
resolve_local_openai_bearer_auth,
|
|
|
|
|
};
|
2026-09-14 21:17:21 +08:00
|
|
|
use super::network::{
|
|
|
|
|
resolve_transport_execution_timeouts, resolve_transport_profile,
|
|
|
|
|
resolve_transport_proxy_snapshot,
|
|
|
|
|
};
|
2026-05-02 13:23:54 +08:00
|
|
|
use super::policy::{
|
|
|
|
|
local_gemini_transport_unsupported_reason_with_network,
|
2026-09-14 21:17:21 +08:00
|
|
|
local_standard_transport_unsupported_reason_with_network,
|
2026-05-02 13:23:54 +08:00
|
|
|
};
|
2026-05-06 23:48:31 +08:00
|
|
|
use super::rules::{
|
|
|
|
|
apply_local_body_rules_with_request_headers, apply_local_header_rules_with_request_headers,
|
|
|
|
|
};
|
2026-04-05 20:23:16 +08:00
|
|
|
use super::snapshot::GatewayProviderTransportSnapshot;
|
2026-05-02 13:23:54 +08:00
|
|
|
use super::url::{build_gemini_video_predict_long_running_url, build_passthrough_path_url};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum ProviderVideoCreateFamily {
|
|
|
|
|
OpenAi,
|
|
|
|
|
Gemini,
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
#[derive(Clone, Copy)]
|
2026-05-02 13:23:54 +08:00
|
|
|
pub struct ProviderVideoCreateHeadersInput<'a> {
|
2026-09-14 21:17:21 +08:00
|
|
|
pub transport: &'a GatewayProviderTransportSnapshot,
|
2026-05-02 13:23:54 +08:00
|
|
|
pub headers: &'a http::HeaderMap,
|
|
|
|
|
pub auth_header: &'a str,
|
|
|
|
|
pub auth_value: &'a str,
|
|
|
|
|
pub header_rules: Option<&'a Value>,
|
|
|
|
|
pub provider_request_body: &'a Value,
|
|
|
|
|
pub original_request_body: &'a Value,
|
|
|
|
|
}
|
2026-04-05 20:23:16 +08:00
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
impl fmt::Debug for ProviderVideoCreateHeadersInput<'_> {
|
|
|
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
formatter
|
|
|
|
|
.debug_struct("ProviderVideoCreateHeadersInput")
|
|
|
|
|
.field(
|
|
|
|
|
"request_header_names",
|
|
|
|
|
&self
|
|
|
|
|
.headers
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|name| name.as_str())
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
)
|
|
|
|
|
.field("auth_header", &self.auth_header)
|
|
|
|
|
.field("has_auth_value", &(!self.auth_value.is_empty()))
|
|
|
|
|
.field("has_header_rules", &self.header_rules.is_some())
|
|
|
|
|
.field(
|
|
|
|
|
"provider_request_body_bytes",
|
|
|
|
|
&serde_json::to_vec(self.provider_request_body)
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|bytes| bytes.len()),
|
|
|
|
|
)
|
|
|
|
|
.field(
|
|
|
|
|
"original_request_body_bytes",
|
|
|
|
|
&serde_json::to_vec(self.original_request_body)
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|bytes| bytes.len()),
|
|
|
|
|
)
|
|
|
|
|
.finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
#[async_trait]
|
|
|
|
|
pub trait VideoTaskTransportSnapshotLookup: Send + Sync {
|
|
|
|
|
async fn read_video_task_provider_transport_snapshot(
|
|
|
|
|
&self,
|
|
|
|
|
provider_id: &str,
|
|
|
|
|
endpoint_id: &str,
|
|
|
|
|
key_id: &str,
|
|
|
|
|
) -> Result<Option<GatewayProviderTransportSnapshot>, String>;
|
2026-09-14 21:17:21 +08:00
|
|
|
|
|
|
|
|
async fn resolve_video_task_proxy(
|
|
|
|
|
&self,
|
|
|
|
|
transport: &GatewayProviderTransportSnapshot,
|
|
|
|
|
) -> Option<ProxySnapshot> {
|
|
|
|
|
resolve_transport_proxy_snapshot(transport)
|
|
|
|
|
}
|
2026-04-05 20:23:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn resolve_local_video_task_transport(
|
|
|
|
|
transport: &GatewayProviderTransportSnapshot,
|
|
|
|
|
api_format: &str,
|
|
|
|
|
model_name: Option<String>,
|
|
|
|
|
) -> Option<LocalVideoTaskTransport> {
|
|
|
|
|
let api_format = api_format.trim();
|
|
|
|
|
let (auth_header, auth_value) = match api_format {
|
|
|
|
|
"openai:video" => {
|
2026-09-14 21:17:21 +08:00
|
|
|
if local_standard_transport_unsupported_reason_with_network(transport, api_format)
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
2026-04-05 20:23:16 +08:00
|
|
|
return None;
|
|
|
|
|
}
|
2026-09-14 21:17:21 +08:00
|
|
|
resolve_openai_compatible_video_auth(transport)?
|
2026-04-05 20:23:16 +08:00
|
|
|
}
|
|
|
|
|
"gemini:video" => {
|
2026-09-14 21:17:21 +08:00
|
|
|
if local_gemini_transport_unsupported_reason_with_network(transport, api_format)
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
2026-04-05 20:23:16 +08:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
resolve_local_gemini_auth(transport)?
|
|
|
|
|
}
|
|
|
|
|
_ => return None,
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
let mut resolved =
|
|
|
|
|
LocalVideoTaskTransport::from_bridge_input(LocalVideoTaskTransportBridgeInput {
|
|
|
|
|
upstream_base_url: crate::xai::resolved_xai_request_base_url(transport, api_format),
|
2026-04-05 20:23:16 +08:00
|
|
|
provider_name: Some(transport.provider.name.clone()),
|
|
|
|
|
provider_id: transport.provider.id.clone(),
|
|
|
|
|
endpoint_id: transport.endpoint.id.clone(),
|
|
|
|
|
key_id: transport.key.id.clone(),
|
|
|
|
|
auth_header,
|
|
|
|
|
auth_value,
|
|
|
|
|
content_type: Some("application/json".to_string()),
|
|
|
|
|
model_name,
|
2026-09-14 21:17:21 +08:00
|
|
|
proxy: resolve_transport_proxy_snapshot(transport),
|
2026-05-06 13:54:33 +08:00
|
|
|
transport_profile: resolve_transport_profile(transport),
|
2026-04-05 20:23:16 +08:00
|
|
|
timeouts: resolve_transport_execution_timeouts(transport),
|
2026-09-14 21:17:21 +08:00
|
|
|
});
|
|
|
|
|
crate::xai::insert_cli_identity_headers_if_needed(transport, api_format, &mut resolved.headers);
|
|
|
|
|
Some(resolved)
|
2026-04-05 20:23:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
pub fn video_create_transport_unsupported_reason(
|
|
|
|
|
transport: &GatewayProviderTransportSnapshot,
|
|
|
|
|
family: ProviderVideoCreateFamily,
|
|
|
|
|
api_format: &str,
|
|
|
|
|
) -> Option<&'static str> {
|
|
|
|
|
match family {
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi => {
|
|
|
|
|
local_standard_transport_unsupported_reason_with_network(transport, api_format)
|
|
|
|
|
}
|
|
|
|
|
ProviderVideoCreateFamily::Gemini => {
|
|
|
|
|
local_gemini_transport_unsupported_reason_with_network(transport, api_format)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn resolve_video_create_auth(
|
|
|
|
|
transport: &GatewayProviderTransportSnapshot,
|
|
|
|
|
family: ProviderVideoCreateFamily,
|
|
|
|
|
) -> Option<(String, String)> {
|
|
|
|
|
match family {
|
2026-09-14 21:17:21 +08:00
|
|
|
ProviderVideoCreateFamily::OpenAi => resolve_openai_compatible_video_auth(transport),
|
2026-05-02 13:23:54 +08:00
|
|
|
ProviderVideoCreateFamily::Gemini => resolve_local_gemini_auth(transport),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn build_video_create_request_body(
|
|
|
|
|
body_json: &Value,
|
|
|
|
|
family: ProviderVideoCreateFamily,
|
|
|
|
|
mapped_model: &str,
|
|
|
|
|
body_rules: Option<&Value>,
|
2026-05-06 23:48:31 +08:00
|
|
|
request_headers: Option<&http::HeaderMap>,
|
2026-05-02 13:23:54 +08:00
|
|
|
) -> Option<Value> {
|
|
|
|
|
let mut provider_request_body = match family {
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi => {
|
|
|
|
|
let mut provider_request_body = body_json.as_object().cloned().unwrap_or_default();
|
|
|
|
|
provider_request_body
|
|
|
|
|
.insert("model".to_string(), Value::String(mapped_model.to_string()));
|
|
|
|
|
Value::Object(provider_request_body)
|
|
|
|
|
}
|
|
|
|
|
ProviderVideoCreateFamily::Gemini => body_json.clone(),
|
|
|
|
|
};
|
2026-05-06 23:48:31 +08:00
|
|
|
if !apply_local_body_rules_with_request_headers(
|
|
|
|
|
&mut provider_request_body,
|
|
|
|
|
body_rules,
|
|
|
|
|
Some(body_json),
|
|
|
|
|
request_headers,
|
|
|
|
|
) {
|
2026-05-02 13:23:54 +08:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
Some(provider_request_body)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
fn resolve_openai_compatible_video_auth(
|
|
|
|
|
transport: &GatewayProviderTransportSnapshot,
|
|
|
|
|
) -> Option<(String, String)> {
|
|
|
|
|
resolve_local_openai_bearer_auth(transport).or_else(|| {
|
|
|
|
|
crate::generic_oauth::resolve_local_generic_oauth_transport_authorization(transport)
|
|
|
|
|
.map(|value| ("authorization".to_string(), value))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
pub fn build_video_create_upstream_url(
|
|
|
|
|
transport: &GatewayProviderTransportSnapshot,
|
|
|
|
|
request_path: &str,
|
|
|
|
|
request_query: Option<&str>,
|
|
|
|
|
mapped_model: &str,
|
|
|
|
|
family: ProviderVideoCreateFamily,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let custom_path = transport
|
|
|
|
|
.endpoint
|
|
|
|
|
.custom_path
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty());
|
|
|
|
|
|
|
|
|
|
if let Some(path) = custom_path {
|
|
|
|
|
let blocked_keys = match family {
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi => &[][..],
|
|
|
|
|
ProviderVideoCreateFamily::Gemini => &["key"][..],
|
|
|
|
|
};
|
|
|
|
|
return build_passthrough_path_url(
|
2026-09-14 21:17:21 +08:00
|
|
|
&crate::xai::resolved_xai_request_base_url(
|
|
|
|
|
transport,
|
|
|
|
|
match family {
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi => "openai:video",
|
|
|
|
|
ProviderVideoCreateFamily::Gemini => "gemini:video",
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-05-02 13:23:54 +08:00
|
|
|
path,
|
|
|
|
|
request_query,
|
|
|
|
|
blocked_keys,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match family {
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi => build_passthrough_path_url(
|
2026-09-14 21:17:21 +08:00
|
|
|
&crate::xai::resolved_xai_request_base_url(transport, "openai:video"),
|
|
|
|
|
if crate::xai::is_xai_provider_transport(transport)
|
|
|
|
|
&& matches!(request_path, "/v1/videos" | "/openai/v1/videos")
|
|
|
|
|
{
|
|
|
|
|
"/videos/generations"
|
|
|
|
|
} else {
|
|
|
|
|
openai_video_api_root_request_path(request_path)
|
|
|
|
|
},
|
2026-05-02 13:23:54 +08:00
|
|
|
request_query,
|
|
|
|
|
&[],
|
|
|
|
|
),
|
|
|
|
|
ProviderVideoCreateFamily::Gemini => build_gemini_video_predict_long_running_url(
|
|
|
|
|
&transport.endpoint.base_url,
|
|
|
|
|
mapped_model,
|
|
|
|
|
request_query,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 02:29:33 +08:00
|
|
|
fn openai_video_api_root_request_path(request_path: &str) -> &str {
|
2026-09-14 21:17:21 +08:00
|
|
|
let request_path = request_path.strip_prefix("/openai").unwrap_or(request_path);
|
2026-05-29 02:29:33 +08:00
|
|
|
if request_path.starts_with("/v1/") {
|
|
|
|
|
&request_path[3..]
|
|
|
|
|
} else {
|
|
|
|
|
request_path
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
pub fn build_video_create_headers(
|
|
|
|
|
input: ProviderVideoCreateHeadersInput<'_>,
|
|
|
|
|
) -> Option<BTreeMap<String, String>> {
|
|
|
|
|
let mut provider_request_headers = build_passthrough_headers_with_auth(
|
|
|
|
|
input.headers,
|
|
|
|
|
input.auth_header,
|
|
|
|
|
input.auth_value,
|
|
|
|
|
&BTreeMap::new(),
|
|
|
|
|
);
|
2026-09-14 21:17:21 +08:00
|
|
|
crate::xai::insert_cli_identity_headers_if_needed(
|
|
|
|
|
input.transport,
|
|
|
|
|
"openai:video",
|
|
|
|
|
&mut provider_request_headers,
|
|
|
|
|
);
|
2026-05-06 23:48:31 +08:00
|
|
|
if !apply_local_header_rules_with_request_headers(
|
2026-05-02 13:23:54 +08:00
|
|
|
&mut provider_request_headers,
|
|
|
|
|
input.header_rules,
|
|
|
|
|
&[input.auth_header, "content-type"],
|
|
|
|
|
input.provider_request_body,
|
|
|
|
|
Some(input.original_request_body),
|
2026-05-06 23:48:31 +08:00
|
|
|
Some(input.headers),
|
2026-05-02 13:23:54 +08:00
|
|
|
) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-09-04 03:45:52 +08:00
|
|
|
let declared_connection_headers =
|
|
|
|
|
super::headers::declared_connection_header_names(input.headers, &BTreeMap::new());
|
|
|
|
|
super::headers::remove_declared_connection_headers(
|
|
|
|
|
&mut provider_request_headers,
|
|
|
|
|
&declared_connection_headers,
|
|
|
|
|
);
|
2026-05-02 13:23:54 +08:00
|
|
|
Some(provider_request_headers)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
pub async fn reconstruct_local_video_task_snapshot(
|
|
|
|
|
lookup: &dyn VideoTaskTransportSnapshotLookup,
|
|
|
|
|
task: &StoredVideoTask,
|
|
|
|
|
) -> Result<Option<LocalVideoTaskSnapshot>, String> {
|
|
|
|
|
let provider_api_format = task
|
|
|
|
|
.provider_api_format
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.trim();
|
|
|
|
|
if !matches!(provider_api_format, "openai:video" | "gemini:video") {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some(provider_id) = task.provider_id.as_deref() else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
let Some(endpoint_id) = task.endpoint_id.as_deref() else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
let Some(key_id) = task.key_id.as_deref() else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(transport) = lookup
|
|
|
|
|
.read_video_task_provider_transport_snapshot(provider_id, endpoint_id, key_id)
|
|
|
|
|
.await?
|
|
|
|
|
else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
let Some(mut local_transport) =
|
2026-04-05 20:23:16 +08:00
|
|
|
resolve_local_video_task_transport(&transport, provider_api_format, task.model.clone())
|
|
|
|
|
else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
// Resolve deployment-managed nodes, system defaults and tunnel affinity just as
|
|
|
|
|
// creation does; serialized task metadata intentionally contains no credentials.
|
|
|
|
|
local_transport.proxy = lookup.resolve_video_task_proxy(&transport).await;
|
|
|
|
|
|
|
|
|
|
let mut snapshot =
|
|
|
|
|
LocalVideoTaskSnapshot::from_stored_task_with_transport(task, local_transport);
|
|
|
|
|
if let Some(LocalVideoTaskSnapshot::OpenAi(seed)) = &mut snapshot {
|
|
|
|
|
seed.xai_provider = crate::xai::is_xai_provider_transport(&transport);
|
|
|
|
|
}
|
|
|
|
|
Ok(snapshot)
|
2026-04-05 20:23:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-04-07 02:50:19 +08:00
|
|
|
use aether_data_contracts::repository::video_tasks::{StoredVideoTask, VideoTaskStatus};
|
2026-04-05 20:23:16 +08:00
|
|
|
use aether_video_tasks_core::LocalVideoTaskSnapshot;
|
|
|
|
|
use async_trait::async_trait;
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
|
|
|
use super::{
|
2026-05-02 13:23:54 +08:00
|
|
|
build_video_create_headers, build_video_create_request_body,
|
|
|
|
|
build_video_create_upstream_url, reconstruct_local_video_task_snapshot,
|
|
|
|
|
resolve_local_video_task_transport, ProviderVideoCreateFamily,
|
|
|
|
|
ProviderVideoCreateHeadersInput, VideoTaskTransportSnapshotLookup,
|
2026-04-05 20:23:16 +08:00
|
|
|
};
|
|
|
|
|
use crate::snapshot::{
|
|
|
|
|
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
|
|
|
|
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn sample_transport(api_format: &str, auth_type: &str) -> GatewayProviderTransportSnapshot {
|
|
|
|
|
GatewayProviderTransportSnapshot {
|
|
|
|
|
provider: GatewayProviderTransportProvider {
|
|
|
|
|
id: "provider-1".to_string(),
|
|
|
|
|
name: "Provider One".to_string(),
|
|
|
|
|
provider_type: "openai".to_string(),
|
|
|
|
|
website: None,
|
|
|
|
|
is_active: true,
|
|
|
|
|
keep_priority_on_conversion: false,
|
|
|
|
|
enable_format_conversion: false,
|
|
|
|
|
concurrent_limit: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
|
proxy: None,
|
|
|
|
|
request_timeout_secs: Some(30.0),
|
|
|
|
|
stream_first_byte_timeout_secs: Some(5.0),
|
|
|
|
|
config: None,
|
|
|
|
|
},
|
|
|
|
|
endpoint: GatewayProviderTransportEndpoint {
|
|
|
|
|
id: "endpoint-1".to_string(),
|
|
|
|
|
provider_id: "provider-1".to_string(),
|
|
|
|
|
api_format: api_format.to_string(),
|
|
|
|
|
api_family: None,
|
|
|
|
|
endpoint_kind: None,
|
|
|
|
|
is_active: true,
|
|
|
|
|
base_url: "https://example.com".to_string(),
|
|
|
|
|
header_rules: None,
|
|
|
|
|
body_rules: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
|
custom_path: None,
|
|
|
|
|
config: None,
|
|
|
|
|
format_acceptance_config: None,
|
|
|
|
|
proxy: None,
|
|
|
|
|
},
|
|
|
|
|
key: GatewayProviderTransportKey {
|
|
|
|
|
id: "key-1".to_string(),
|
|
|
|
|
provider_id: "provider-1".to_string(),
|
|
|
|
|
name: "key".to_string(),
|
|
|
|
|
auth_type: auth_type.to_string(),
|
|
|
|
|
is_active: true,
|
|
|
|
|
api_formats: None,
|
2026-04-29 15:46:50 +08:00
|
|
|
auth_type_by_format: None,
|
2026-05-03 00:49:22 +08:00
|
|
|
allow_auth_channel_mismatch_formats: None,
|
2026-04-29 15:46:50 +08:00
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
allowed_models: None,
|
|
|
|
|
capabilities: None,
|
|
|
|
|
rate_multipliers: None,
|
|
|
|
|
global_priority_by_format: None,
|
|
|
|
|
expires_at_unix_secs: None,
|
|
|
|
|
proxy: None,
|
|
|
|
|
fingerprint: None,
|
2026-05-21 15:38:10 +08:00
|
|
|
upstream_metadata: None,
|
2026-04-05 20:23:16 +08:00
|
|
|
decrypted_api_key: "secret".to_string(),
|
|
|
|
|
decrypted_auth_config: None,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sample_stored_video_task() -> StoredVideoTask {
|
|
|
|
|
StoredVideoTask {
|
|
|
|
|
id: "task-1".to_string(),
|
|
|
|
|
short_id: Some("short-1".to_string()),
|
|
|
|
|
request_id: "request-1".to_string(),
|
|
|
|
|
user_id: Some("user-1".to_string()),
|
|
|
|
|
api_key_id: Some("api-key-1".to_string()),
|
|
|
|
|
username: Some("user".to_string()),
|
|
|
|
|
api_key_name: Some("key".to_string()),
|
|
|
|
|
external_task_id: Some("upstream-task-1".to_string()),
|
|
|
|
|
provider_id: Some("provider-1".to_string()),
|
|
|
|
|
endpoint_id: Some("endpoint-1".to_string()),
|
|
|
|
|
key_id: Some("key-1".to_string()),
|
|
|
|
|
client_api_format: Some("openai:video".to_string()),
|
|
|
|
|
provider_api_format: Some("openai:video".to_string()),
|
|
|
|
|
format_converted: false,
|
|
|
|
|
model: Some("sora".to_string()),
|
|
|
|
|
prompt: Some("generate".to_string()),
|
|
|
|
|
original_request_body: Some(json!({"prompt": "generate"})),
|
|
|
|
|
duration_seconds: None,
|
|
|
|
|
resolution: None,
|
|
|
|
|
aspect_ratio: None,
|
|
|
|
|
size: Some("1024x1024".to_string()),
|
|
|
|
|
status: VideoTaskStatus::Submitted,
|
|
|
|
|
progress_percent: 0,
|
|
|
|
|
progress_message: None,
|
|
|
|
|
retry_count: 0,
|
|
|
|
|
poll_interval_seconds: 10,
|
|
|
|
|
next_poll_at_unix_secs: None,
|
|
|
|
|
poll_count: 0,
|
|
|
|
|
max_poll_count: 360,
|
2026-04-10 01:46:14 +08:00
|
|
|
created_at_unix_ms: 1,
|
2026-04-05 20:23:16 +08:00
|
|
|
submitted_at_unix_secs: Some(1),
|
|
|
|
|
completed_at_unix_secs: None,
|
|
|
|
|
updated_at_unix_secs: 1,
|
|
|
|
|
error_code: None,
|
|
|
|
|
error_message: None,
|
|
|
|
|
video_url: None,
|
|
|
|
|
request_metadata: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct TestLookup(Option<GatewayProviderTransportSnapshot>);
|
|
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
|
impl VideoTaskTransportSnapshotLookup for TestLookup {
|
|
|
|
|
async fn read_video_task_provider_transport_snapshot(
|
|
|
|
|
&self,
|
|
|
|
|
_provider_id: &str,
|
|
|
|
|
_endpoint_id: &str,
|
|
|
|
|
_key_id: &str,
|
|
|
|
|
) -> Result<Option<GatewayProviderTransportSnapshot>, String> {
|
|
|
|
|
Ok(self.0.clone())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn resolves_openai_video_transport() {
|
|
|
|
|
let transport = resolve_local_video_task_transport(
|
2026-04-17 11:10:05 +08:00
|
|
|
&sample_transport("openai:video", "api_key"),
|
2026-04-05 20:23:16 +08:00
|
|
|
"openai:video",
|
|
|
|
|
Some("sora".to_string()),
|
|
|
|
|
)
|
|
|
|
|
.expect("transport");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
transport.headers.get("authorization").map(String::as_str),
|
|
|
|
|
Some("Bearer secret")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(transport.model_name.as_deref(), Some("sora"));
|
|
|
|
|
assert_eq!(transport.provider_id, "provider-1");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn reconstructs_video_with_configured_proxy_and_profile() {
|
|
|
|
|
let mut transport = sample_transport("openai:video", "oauth");
|
|
|
|
|
transport.provider.provider_type = "xai".into();
|
|
|
|
|
transport.endpoint.base_url = "https://cli-chat-proxy.grok.com/v1".into();
|
|
|
|
|
transport.provider.proxy = Some(json!({"enabled":true,"url":"http://127.0.0.1:9876"}));
|
|
|
|
|
transport.provider.config = Some(json!({"fingerprint":{"transport_profile":{
|
|
|
|
|
"profile_id":"test-video","backend":"reqwest_rustls","http_mode":"auto","pool_scope":"key"
|
|
|
|
|
}}}));
|
|
|
|
|
transport.key.decrypted_auth_config = Some(r#"{"using_api":false}"#.into());
|
|
|
|
|
let lookup = TestLookup(Some(transport));
|
|
|
|
|
let snapshot = reconstruct_local_video_task_snapshot(&lookup, &sample_stored_video_task())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap()
|
|
|
|
|
.expect("proxied video must resume after restart");
|
|
|
|
|
let LocalVideoTaskSnapshot::OpenAi(seed) = snapshot else {
|
|
|
|
|
panic!("expected OpenAI video")
|
|
|
|
|
};
|
|
|
|
|
assert!(seed.xai_provider);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
seed.transport.proxy.as_ref().unwrap().url.as_deref(),
|
|
|
|
|
Some("http://127.0.0.1:9876/")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
seed.transport
|
|
|
|
|
.transport_profile
|
|
|
|
|
.as_ref()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.profile_id,
|
|
|
|
|
"test-video"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
seed.transport
|
|
|
|
|
.headers
|
|
|
|
|
.get("x-xai-token-auth")
|
|
|
|
|
.map(String::as_str),
|
|
|
|
|
Some("xai-grok-cli")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
#[test]
|
|
|
|
|
fn resolves_gemini_video_transport() {
|
|
|
|
|
let transport = resolve_local_video_task_transport(
|
|
|
|
|
&sample_transport("gemini:video", "api_key"),
|
|
|
|
|
"gemini:video",
|
|
|
|
|
Some("veo".to_string()),
|
|
|
|
|
)
|
|
|
|
|
.expect("transport");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
transport.headers.get("x-goog-api-key").map(String::as_str),
|
|
|
|
|
Some("secret")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(transport.model_name.as_deref(), Some("veo"));
|
|
|
|
|
assert_eq!(transport.endpoint_id, "endpoint-1");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
#[test]
|
|
|
|
|
fn builds_openai_video_create_request_body_with_mapped_model() {
|
|
|
|
|
let body = build_video_create_request_body(
|
|
|
|
|
&json!({"prompt": "make a clip", "model": "client-model"}),
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi,
|
|
|
|
|
"upstream-video-model",
|
|
|
|
|
None,
|
2026-05-06 23:48:31 +08:00
|
|
|
None,
|
2026-05-02 13:23:54 +08:00
|
|
|
)
|
|
|
|
|
.expect("body should build");
|
|
|
|
|
|
|
|
|
|
assert_eq!(body.get("prompt"), Some(&json!("make a clip")));
|
|
|
|
|
assert_eq!(body.get("model"), Some(&json!("upstream-video-model")));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 02:29:33 +08:00
|
|
|
#[test]
|
|
|
|
|
fn builds_openai_video_create_url_from_api_root_base() {
|
|
|
|
|
let mut transport = sample_transport("openai:video", "bearer");
|
|
|
|
|
transport.endpoint.base_url = "https://api.openai.example/v1".to_string();
|
|
|
|
|
let url = build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/v1/videos",
|
|
|
|
|
Some("trace=1"),
|
|
|
|
|
"sora-upstream",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi,
|
|
|
|
|
)
|
|
|
|
|
.expect("url should build");
|
|
|
|
|
|
|
|
|
|
assert_eq!(url, "https://api.openai.example/v1/videos?trace=1");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 21:17:21 +08:00
|
|
|
#[test]
|
|
|
|
|
fn xai_video_create_paths_preserve_auth_hosts_and_custom_endpoints() {
|
|
|
|
|
for (auth, base) in [
|
|
|
|
|
("oauth", "https://cli-chat-proxy.grok.com/v1"),
|
|
|
|
|
("api_key", "https://api.x.ai/v1"),
|
|
|
|
|
] {
|
|
|
|
|
let mut transport = sample_transport("openai:video", auth);
|
|
|
|
|
transport.provider.provider_type = "xai".into();
|
|
|
|
|
transport.endpoint.base_url = "https://cli-chat-proxy.grok.com/v1".into();
|
|
|
|
|
transport.key.decrypted_auth_config =
|
|
|
|
|
(auth == "oauth").then(|| r#"{"using_api":false}"#.into());
|
|
|
|
|
for path in ["/v1/videos", "/openai/v1/videos", "/v1/videos/generations"] {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
path,
|
|
|
|
|
Some("trace=1"),
|
|
|
|
|
"grok-imagine-video",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
format!("{base}/videos/generations?trace=1")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
transport.endpoint.base_url = "https://gateway.example/prefix/v1".into();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/openai/v1/videos",
|
|
|
|
|
None,
|
|
|
|
|
"grok-imagine-video",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
"https://gateway.example/prefix/v1/videos/generations"
|
|
|
|
|
);
|
|
|
|
|
transport.endpoint.custom_path = Some("/custom/videos/generations".into());
|
|
|
|
|
let url = build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/openai/v1/videos",
|
|
|
|
|
None,
|
|
|
|
|
"grok-imagine-video",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(url.ends_with("/custom/videos/generations"), "{url}");
|
|
|
|
|
}
|
|
|
|
|
let transport = sample_transport("openai:video", "api_key");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/openai/v1/videos",
|
|
|
|
|
None,
|
|
|
|
|
"sora",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi
|
|
|
|
|
),
|
|
|
|
|
build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/v1/videos",
|
|
|
|
|
None,
|
|
|
|
|
"sora",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn xai_oauth_video_uses_cli_proxy() {
|
|
|
|
|
let mut transport = sample_transport("openai:video", "oauth");
|
|
|
|
|
transport.provider.provider_type = "xai".to_string();
|
|
|
|
|
transport.endpoint.base_url = "https://cli-chat-proxy.grok.com/v1".to_string();
|
|
|
|
|
transport.key.decrypted_auth_config =
|
|
|
|
|
Some(r#"{"refresh_token":"rt","using_api":false}"#.to_string());
|
|
|
|
|
let url = build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/v1/videos/generations",
|
|
|
|
|
None,
|
|
|
|
|
"grok-imagine-video",
|
|
|
|
|
ProviderVideoCreateFamily::OpenAi,
|
|
|
|
|
)
|
|
|
|
|
.expect("url should build");
|
|
|
|
|
|
|
|
|
|
assert_eq!(url, "https://cli-chat-proxy.grok.com/v1/videos/generations");
|
|
|
|
|
let headers = build_video_create_headers(ProviderVideoCreateHeadersInput {
|
|
|
|
|
transport: &transport,
|
|
|
|
|
headers: &http::HeaderMap::new(),
|
|
|
|
|
auth_header: "authorization",
|
|
|
|
|
auth_value: "Bearer test-token",
|
|
|
|
|
header_rules: None,
|
|
|
|
|
provider_request_body: &json!({"prompt": "A cat"}),
|
|
|
|
|
original_request_body: &json!({"prompt": "A cat"}),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
headers.get("x-xai-token-auth").map(String::as_str),
|
|
|
|
|
Some("xai-grok-cli")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let reconstructed = super::resolve_local_video_task_transport(
|
|
|
|
|
&transport,
|
|
|
|
|
"openai:video",
|
|
|
|
|
Some("grok-imagine-video".into()),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
reconstructed.upstream_base_url,
|
|
|
|
|
"https://cli-chat-proxy.grok.com/v1"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
reconstructed.headers.get("x-xai-token-auth"),
|
|
|
|
|
headers.get("x-xai-token-auth")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
#[test]
|
|
|
|
|
fn builds_gemini_video_create_url_and_removes_client_key_query() {
|
|
|
|
|
let transport = sample_transport("gemini:video", "api_key");
|
|
|
|
|
let url = build_video_create_upstream_url(
|
|
|
|
|
&transport,
|
|
|
|
|
"/v1beta/models/client-model:predictLongRunning",
|
|
|
|
|
Some("key=client-key&trace=1"),
|
|
|
|
|
"veo-upstream",
|
|
|
|
|
ProviderVideoCreateFamily::Gemini,
|
|
|
|
|
)
|
|
|
|
|
.expect("url should build");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
url,
|
|
|
|
|
"https://example.com/v1beta/models/veo-upstream:predictLongRunning?trace=1"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn builds_video_create_headers_with_auth_and_rules() {
|
|
|
|
|
let provider_request_body = json!({"prompt": "make a clip"});
|
|
|
|
|
let original_request_body = provider_request_body.clone();
|
|
|
|
|
let headers = build_video_create_headers(ProviderVideoCreateHeadersInput {
|
2026-09-14 21:17:21 +08:00
|
|
|
transport: &sample_transport("openai:video", "bearer"),
|
2026-05-02 13:23:54 +08:00
|
|
|
headers: &http::HeaderMap::new(),
|
|
|
|
|
auth_header: "authorization",
|
|
|
|
|
auth_value: "Bearer secret",
|
|
|
|
|
header_rules: Some(&json!([
|
|
|
|
|
{"action":"set","key":"x-provider-tag","value":"video"}
|
|
|
|
|
])),
|
|
|
|
|
provider_request_body: &provider_request_body,
|
|
|
|
|
original_request_body: &original_request_body,
|
|
|
|
|
})
|
|
|
|
|
.expect("headers should build");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
headers.get("authorization").map(String::as_str),
|
|
|
|
|
Some("Bearer secret")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
headers.get("x-provider-tag").map(String::as_str),
|
|
|
|
|
Some("video")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
#[test]
|
|
|
|
|
fn rejects_mismatched_video_transport_format() {
|
|
|
|
|
let transport = sample_transport("openai:chat", "bearer");
|
|
|
|
|
assert!(resolve_local_video_task_transport(&transport, "openai:video", None).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn reconstructs_openai_video_snapshot_via_lookup_trait() {
|
|
|
|
|
let lookup = TestLookup(Some(sample_transport("openai:video", "bearer")));
|
|
|
|
|
let snapshot = reconstruct_local_video_task_snapshot(&lookup, &sample_stored_video_task())
|
|
|
|
|
.await
|
|
|
|
|
.expect("lookup should succeed")
|
|
|
|
|
.expect("snapshot");
|
|
|
|
|
|
|
|
|
|
match snapshot {
|
|
|
|
|
LocalVideoTaskSnapshot::OpenAi(seed) => {
|
|
|
|
|
assert_eq!(seed.transport.provider_id, "provider-1");
|
|
|
|
|
assert_eq!(seed.transport.model_name.as_deref(), Some("sora"));
|
|
|
|
|
}
|
|
|
|
|
LocalVideoTaskSnapshot::Gemini(_) => panic!("expected openai snapshot"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|