gproxy_protocol/transform/guardian/
request.rs1use 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#[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}