gproxy-protocol 4.1.14

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use crate::{
    transform::{Converted, Report, TransformError},
    wire::{DeclaredFields, gemini::video as g, openai::video as o},
};
use std::collections::BTreeMap;

/// A host-registered mapping; source identity must match this operation before
/// advertising its proxy polling URL. Generated bytes require published URLs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenAiVideoResponseContext {
    pub operation_name: String,
    pub polling_url: String,
    pub resources: BTreeMap<String, super::request::ResolvedVideoResource>,
}

/// Reverse mapping must use an actual Gemini-facing operation resource owned
/// by the host, never reinterpret an arbitrary OpenRouter job ID as its name.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeminiOperationContext {
    pub source_id: String,
    pub source_polling_url: String,
    pub operation_name: String,
}

pub(super) fn operation_name(value: &str) -> Result<(), TransformError> {
    let segments: Vec<_> = value.split('/').collect();
    if segments.len() < 2
        || segments[segments.len() - 2] != "operations"
        || segments
            .iter()
            .any(|v| v.is_empty() || matches!(*v, "." | ".."))
        || value.contains(['?', '#', '\\'])
        || value.chars().any(char::is_control)
    {
        return Err(TransformError::shape(
            "operation.name",
            "valid operations/{id} resource required",
        ));
    }
    Ok(())
}

pub fn gemini_operation_to_openai_response(
    input: g::VideoOperation,
    context: &OpenAiVideoResponseContext,
) -> Result<Converted<o::VideoGenerationResponseBody>, TransformError> {
    let input = input.into_declared();
    let id = input
        .name
        .as_ref()
        .filter(|v| !v.is_empty())
        .ok_or_else(|| TransformError::missing_metadata("operation.name"))?;
    operation_name(id)?;
    if id != &context.operation_name {
        return Err(TransformError::shape(
            "operation.binding",
            "operation name differs from registered polling context",
        ));
    }
    let polling_url = super::resources::url(&context.polling_url)?;
    if input.error.is_some() && input.response.is_some() {
        return Err(TransformError::invalid_result(
            "operation",
            "error and response cannot coexist",
        ));
    }
    if input.done != Some(true) && (input.error.is_some() || input.response.is_some()) {
        return Err(TransformError::invalid_result(
            "operation.done",
            "result/error on pending operation",
        ));
    }
    let mut report = Report::default();
    if input.metadata.is_some() {
        report.omitted("metadata", "OpenRouter response lacks operation metadata");
    }
    let (status, error, urls) = if let Some(error) = input.error {
        let message = error
            .message
            .filter(|v| !v.is_empty())
            .or_else(|| error.code.map(|v| format!("Google RPC code {v}")))
            .ok_or_else(|| TransformError::missing_metadata("operation.error message/code"))?;
        if error.details.is_some() {
            report.omitted("error.details", "OpenRouter exposes error text only");
        }
        (o::VideoStatus::Failed, Some(message), None)
    } else if input.done == Some(true) {
        let result = input
            .response
            .and_then(|r| r.generate_video_response)
            .ok_or_else(|| {
                TransformError::invalid_result(
                    "response.generateVideoResponse",
                    "completed operation has no video result",
                )
            })?;
        let filtered = result.rai_media_filtered_count.unwrap_or(0);
        if filtered < 0 {
            return Err(TransformError::invalid_result(
                "raiMediaFilteredCount",
                "negative count",
            ));
        }
        let mut urls = Vec::new();
        for sample in result.generated_samples.unwrap_or_default() {
            let video = sample.video.ok_or_else(|| {
                TransformError::invalid_result("generatedSamples.video", "sample missing video")
            })?;
            urls.push(super::resources::video_url(&video, &context.resources)?);
        }
        if filtered > 0 || result.rai_media_filtered_reasons.is_some() {
            report.omitted(
                "rai_filtering",
                "OpenRouter has no filtered count/reason fields; surviving samples retained",
            );
        }
        if urls.is_empty() {
            if filtered > 0 {
                (
                    o::VideoStatus::Failed,
                    Some(
                        result
                            .rai_media_filtered_reasons
                            .filter(|v| !v.is_empty())
                            .map(|v| v.join("; "))
                            .unwrap_or_else(|| format!("{filtered} video samples filtered")),
                    ),
                    None,
                )
            } else {
                return Err(TransformError::invalid_result(
                    "generatedSamples",
                    "completed operation produced no video",
                ));
            }
        } else {
            (o::VideoStatus::Completed, None, Some(urls))
        }
    } else {
        (o::VideoStatus::InProgress, None, None)
    };
    let mut out = o::VideoGenerationResponseBody::builder(id.clone(), polling_url, status).build();
    out.error = error;
    out.unsigned_urls = urls;
    Ok(Converted { value: out, report })
}

pub fn openai_response_to_gemini_operation(
    input: o::VideoGenerationResponseBody,
    context: &GeminiOperationContext,
) -> Result<Converted<g::VideoOperation>, TransformError> {
    let input = input.into_declared();
    if input.id != context.source_id || input.polling_url != context.source_polling_url {
        return Err(TransformError::shape(
            "operation.binding",
            "OpenRouter id/polling URL differ from stored invocation",
        ));
    }
    super::resources::url(&input.polling_url)?;
    operation_name(&context.operation_name)?;
    let mut out = g::VideoOperation::builder()
        .name(context.operation_name.clone())
        .build();
    let mut report = Report::default();
    match input.status {
        o::VideoStatus::Pending | o::VideoStatus::InProgress => {
            if input.error.is_some() || input.unsigned_urls.as_ref().is_some_and(|v| !v.is_empty())
            {
                return Err(TransformError::invalid_result(
                    "status",
                    "pending response carries terminal error/videos",
                ));
            }
            out.done = Some(false);
        }
        o::VideoStatus::Failed => {
            if input.unsigned_urls.as_ref().is_some_and(|v| !v.is_empty()) {
                return Err(TransformError::invalid_result(
                    "status",
                    "failed video carries successful URLs",
                ));
            }
            let error = input
                .error
                .filter(|v| !v.is_empty())
                .ok_or_else(|| TransformError::missing_metadata("failed video error"))?;
            out.done = Some(true);
            out.error = Some(g::GoogleRpcStatus::builder().message(error).build());
        }
        o::VideoStatus::Completed => {
            if input.error.is_some() {
                return Err(TransformError::invalid_result(
                    "status/error",
                    "completed response has error",
                ));
            }
            let urls = input
                .unsigned_urls
                .filter(|v| !v.is_empty())
                .ok_or_else(|| {
                    TransformError::invalid_result(
                        "unsigned_urls",
                        "completed video requires actual URLs",
                    )
                })?;
            let samples = urls
                .into_iter()
                .map(|url| {
                    super::resources::url(&url).map(|url| {
                        g::GeneratedVideoSample::builder()
                            .video(g::Video::builder().uri(url).build())
                            .build()
                    })
                })
                .filter_map(|value| crate::transform::optional(value).transpose())
                .collect::<Result<Vec<_>, _>>()?;
            out.done = Some(true);
            out.response = Some(
                g::VideoOperationResponse::builder()
                    .generate_video_response(
                        g::GenerateVideoResponse::builder()
                            .generated_samples(samples)
                            .build(),
                    )
                    .build(),
            );
        }
        o::VideoStatus::Cancelled => {
            if input.unsigned_urls.as_ref().is_some_and(|v| !v.is_empty()) {
                return Err(TransformError::invalid_result(
                    "status",
                    "cancelled response carries successful video URLs",
                ));
            }
            out.done = Some(true);
            out.error = Some(
                g::GoogleRpcStatus::builder()
                    .code(1)
                    .message(
                        input
                            .error
                            .unwrap_or_else(|| "Video generation cancelled".into()),
                    )
                    .build(),
            );
        }
        o::VideoStatus::Expired => {
            return Err(TransformError::unsupported(
                "status.expired",
                "expired OpenRouter result has no identical Gemini operation lifecycle",
            ));
        }
    }
    if input.generation_id.is_some() {
        report.omitted(
            "generation_id",
            "Gemini has one operation resource identity",
        );
    }
    if input.usage.is_some() {
        report.omitted("usage", "Gemini operation lacks usage fields");
    }
    Ok(Converted { value: out, report })
}