Skip to main content

gproxy_protocol/transform/guardian/
request.rs

1use crate::{
2    WireRequest,
3    codec::{self, CodecLimits},
4    transform::{Converted, Report, TransformError},
5    wire::{
6        DeclaredFields,
7        claude::{content as cc, count_tokens as ct, generate_content as cg},
8        gemini as g,
9        openai::{chat as o, guardian as source, responses as r},
10    },
11};
12use base64::engine::general_purpose::STANDARD;
13use serde_json::{Value, json};
14
15mod attachments;
16mod binding;
17mod claude;
18mod controls;
19mod evidence;
20mod gemini;
21mod media;
22mod openai_chat;
23mod openai_responses;
24use attachments::attach_media;
25use claude::prepare_claude_plan;
26pub use claude::{prepare_claude, prepare_claude_with_limits};
27use controls::*;
28use evidence::*;
29use gemini::prepare_gemini_plan;
30pub use gemini::{prepare_gemini, prepare_gemini_with_limits};
31use media::*;
32use openai_chat::prepare_openai_chat_plan;
33pub use openai_chat::{prepare_openai_chat, prepare_openai_chat_with_limits};
34use openai_responses::prepare_openai_responses_plan;
35pub use openai_responses::{prepare_openai_responses, prepare_openai_responses_with_limits};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum GuardianOperation {
39    Review,
40    Classify,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct GuardianRequestContext {
45    pub target_model: String,
46    pub max_tokens: i64,
47    pub operation: GuardianOperation,
48}
49
50#[allow(clippy::large_enum_variant)]
51#[derive(Debug)]
52pub enum GuardianDialectRequest {
53    Claude(WireRequest<cg::GenerateContentRequestBody>),
54    Gemini(WireRequest<g::GenerateContentRequestBody>),
55    OpenAiChat(WireRequest<o::GenerateContentRequestBody>),
56    OpenAiResponses(WireRequest<r::GenerateContentRequestBody>),
57}
58
59#[derive(Debug)]
60pub struct GuardianPreparedRequest {
61    operation: GuardianOperation,
62    request: GuardianDialectRequest,
63    source: source::GuardianRequestBody,
64}
65
66impl GuardianPreparedRequest {
67    pub fn source(&self) -> &source::GuardianRequestBody {
68        &self.source
69    }
70    pub fn operation(&self) -> GuardianOperation {
71        self.operation
72    }
73    pub fn request(&self) -> &GuardianDialectRequest {
74        &self.request
75    }
76    pub fn into_request(self) -> GuardianDialectRequest {
77        self.request
78    }
79}
80
81/// Destination API for resource-assisted Guardian preparation.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum GuardianTarget {
84    Claude,
85    Gemini,
86    OpenAiChat,
87    OpenAiResponses,
88}
89
90pub(super) fn request<T>(path: impl Into<String>, body: T) -> WireRequest<T> {
91    WireRequest {
92        method: http::Method::POST,
93        path: path.into(),
94        query: None,
95        headers: http::HeaderMap::new(),
96        body,
97    }
98}
99
100pub fn preflight_request(
101    request: &GuardianDialectRequest,
102    limits: CodecLimits,
103    max_bytes: u64,
104) -> Result<(), TransformError> {
105    let bytes = match request {
106        GuardianDialectRequest::Claude(v) => codec::encode_json(&v.body, limits),
107        GuardianDialectRequest::Gemini(v) => codec::encode_json(&v.body, limits),
108        GuardianDialectRequest::OpenAiChat(v) => codec::encode_json(&v.body, limits),
109        GuardianDialectRequest::OpenAiResponses(v) => codec::encode_json(&v.body, limits),
110    }
111    .map_err(|e| {
112        TransformError::new(
113            if e.kind() == codec::CodecErrorKind::Limit {
114                crate::transform::TransformErrorKind::Limit
115            } else {
116                crate::transform::TransformErrorKind::InvalidInput
117            },
118            "guardian.request",
119            e.to_string(),
120        )
121    })?;
122    if bytes.len() as u64 > max_bytes {
123        return Err(TransformError::new(
124            crate::transform::TransformErrorKind::Limit,
125            "guardian.request",
126            "preflight byte cap exceeded",
127        ));
128    }
129    Ok(())
130}
131
132pub(crate) fn prepare_unattached(
133    input: source::GuardianRequestBody,
134    target: GuardianTarget,
135    context: GuardianRequestContext,
136    limits: CodecLimits,
137) -> Result<(Converted<GuardianPreparedRequest>, Vec<Media>), TransformError> {
138    let prepared = match target {
139        GuardianTarget::Claude => prepare_claude_plan(input, context, limits, false),
140        GuardianTarget::Gemini => prepare_gemini_plan(input, context, limits, false),
141        GuardianTarget::OpenAiChat => prepare_openai_chat_plan(input, context, limits, false),
142        GuardianTarget::OpenAiResponses => {
143            prepare_openai_responses_plan(input, context, limits, false)
144        }
145    }?;
146    let media = media(prepared.value.source())?;
147    Ok((prepared, media))
148}
149
150pub(crate) fn finish_attachments(
151    prepared: &mut GuardianPreparedRequest,
152    media: &[Media],
153    limits: CodecLimits,
154) -> Result<(), TransformError> {
155    attach_media(&mut prepared.request, media)?;
156    preflight_request(&prepared.request, limits, limits.max_body_bytes)
157}