Skip to main content

gproxy_protocol/adapt/
memory.rs

1use crate::{
2    HttpBody, WireResponse,
3    adapt::{JsonInvocation, invoke_json},
4    capability::Upstream,
5    codec::{self, CodecLimits},
6    openai::memory::{MemorySummarizeOutput, RawMemory},
7    transform::{
8        TransformError, TransformErrorKind,
9        memory::{self, MemoryDialectRequest},
10    },
11    wire::DeclaredFields,
12};
13
14#[derive(Debug, Clone, Copy)]
15pub struct MemoryLimits {
16    pub max_bytes: u64,
17    pub codec: CodecLimits,
18}
19
20#[derive(Debug, Clone, Copy)]
21pub enum MemoryDialect {
22    Claude,
23    Gemini,
24    OpenAiChat,
25    OpenAiResponses,
26}
27
28#[derive(Debug)]
29pub enum MemoryFailure {
30    Transform(TransformError),
31    Rejected(Box<WireResponse<HttpBody>>),
32}
33
34#[derive(Debug)]
35pub struct MemoryError {
36    pub attempted_calls: usize,
37    pub completed_traces: usize,
38    pub trace_id: Option<String>,
39    pub failure: MemoryFailure,
40}
41
42impl MemoryError {
43    fn at(error: TransformError, calls: usize, done: usize, id: Option<String>) -> Self {
44        Self {
45            attempted_calls: calls,
46            completed_traces: done,
47            trace_id: id,
48            failure: MemoryFailure::Transform(error),
49        }
50    }
51    pub fn kind(&self) -> TransformErrorKind {
52        match &self.failure {
53            MemoryFailure::Transform(e) => e.kind(),
54            MemoryFailure::Rejected(_) => TransformErrorKind::InvalidResult,
55        }
56    }
57}
58
59impl From<TransformError> for MemoryError {
60    fn from(e: TransformError) -> Self {
61        Self::at(e, 0, 0, None)
62    }
63}
64
65pub async fn summarize<U: Upstream>(
66    upstream: &U,
67    target: &U::Target,
68    dialect: MemoryDialect,
69    traces: Vec<RawMemory>,
70    model: impl Into<String>,
71    max_tokens: i64,
72    limits: MemoryLimits,
73) -> Result<Vec<MemorySummarizeOutput>, MemoryError> {
74    summarize_with_reasoning(
75        upstream, target, dialect, traces, model, max_tokens, None, limits,
76    )
77    .await
78}
79
80#[allow(clippy::too_many_arguments)]
81pub async fn summarize_with_reasoning<U: Upstream>(
82    upstream: &U,
83    target: &U::Target,
84    dialect: MemoryDialect,
85    traces: Vec<RawMemory>,
86    model: impl Into<String>,
87    max_tokens: i64,
88    reasoning: Option<crate::openai::codex::Reasoning>,
89    limits: MemoryLimits,
90) -> Result<Vec<MemorySummarizeOutput>, MemoryError> {
91    let model = model.into();
92    if model.trim().is_empty() || max_tokens <= 0 {
93        return Err(TransformError::shape(
94            "memory.target",
95            "nonempty model and positive token budget required",
96        )
97        .into());
98    }
99
100    let reasoning = reasoning.into_declared();
101    let mut planned = Vec::new();
102    let mut ids = std::collections::BTreeSet::new();
103    let mut total = 0u64;
104    let mut retained = 0u64;
105    let mut bounds = limits.codec;
106    bounds.max_body_bytes = bounds.max_body_bytes.min(upstream.limits().write_bytes);
107    bounds.max_value_bytes = bounds.max_value_bytes.min(bounds.max_body_bytes);
108    bounds.max_buffer_bytes = bounds.max_buffer_bytes.min(bounds.max_body_bytes);
109    // Validate every trace and fully encode-check every target request before the
110    // first side effect; a malformed later trace cannot cause partial execution.
111    for trace in traces {
112        let trace = trace.into_declared();
113
114        if !ids.insert(trace.id.clone()) {
115            return Err(TransformError::shape("trace.id", "duplicate trace identity").into());
116        }
117        let remaining = limits
118            .max_bytes
119            .checked_sub(total)
120            .ok_or_else(|| limit("max_bytes"))?;
121        let mut trace_bounds = bounds;
122        trace_bounds.max_value_bytes = trace_bounds.max_value_bytes.min(remaining);
123        trace_bounds.max_body_bytes = trace_bounds.max_body_bytes.min(remaining);
124        let payload = memory::trace_payload(&trace, trace_bounds)?;
125        total = total
126            .checked_add(payload.len() as u64)
127            .ok_or_else(|| limit("max_bytes"))?;
128        let request = match dialect {
129            MemoryDialect::Claude => {
130                memory::build_claude(payload, model.clone(), max_tokens, reasoning.as_ref())?
131            }
132            MemoryDialect::Gemini => {
133                memory::build_gemini(payload, model.clone(), max_tokens, reasoning.as_ref())?
134            }
135            MemoryDialect::OpenAiChat => {
136                memory::build_openai_chat(payload, model.clone(), max_tokens, reasoning.as_ref())?
137            }
138            MemoryDialect::OpenAiResponses => memory::build_openai_responses(
139                payload,
140                model.clone(),
141                max_tokens,
142                reasoning.as_ref(),
143            )?,
144        };
145        let mut request_bounds = bounds;
146        let remaining = limits
147            .max_bytes
148            .checked_sub(retained)
149            .ok_or_else(|| limit("max_bytes"))?;
150        request_bounds.max_value_bytes = request_bounds.max_value_bytes.min(remaining);
151        request_bounds.max_buffer_bytes = request_bounds.max_buffer_bytes.min(remaining);
152        request_bounds.max_body_bytes = request_bounds.max_body_bytes.min(remaining);
153        let encoded = match &request {
154            MemoryDialectRequest::Claude(v) => codec::encode_json(&v.body, request_bounds),
155            MemoryDialectRequest::Gemini(v) => codec::encode_json(&v.body, request_bounds),
156            MemoryDialectRequest::OpenAiChat(v) => codec::encode_json(&v.body, request_bounds),
157            MemoryDialectRequest::OpenAiResponses(v) => codec::encode_json(&v.body, request_bounds),
158        }
159        .map_err(|e| {
160            TransformError::new(
161                if e.kind() == codec::CodecErrorKind::Limit {
162                    TransformErrorKind::Limit
163                } else {
164                    TransformErrorKind::InvalidInput
165                },
166                "memory.request",
167                e.to_string(),
168            )
169        })?;
170        // Account before retaining the next typed request; a large batch of
171        // tiny traces must not accumulate unbounded repeated schema overhead.
172        retained = retained
173            .checked_add(encoded.len() as u64)
174            .filter(|n| *n <= limits.max_bytes)
175            .ok_or_else(|| limit("max_bytes"))?;
176        planned.push((trace.id, request));
177    }
178    let mut output = Vec::new();
179    let mut output_bytes = 0u64;
180    for (index, (id, request)) in planned.into_iter().enumerate() {
181        let text = match request {
182            MemoryDialectRequest::Claude(request) => finish(
183                invoke_json(upstream, target, request, limits.codec).await,
184                memory::claude_text,
185                index,
186                output.len(),
187                &id,
188            )?,
189            MemoryDialectRequest::Gemini(request) => finish(
190                invoke_json(upstream, target, request, limits.codec).await,
191                memory::gemini_text,
192                index,
193                output.len(),
194                &id,
195            )?,
196            MemoryDialectRequest::OpenAiChat(request) => finish(
197                invoke_json(upstream, target, request, limits.codec).await,
198                memory::chat_text,
199                index,
200                output.len(),
201                &id,
202            )?,
203            MemoryDialectRequest::OpenAiResponses(request) => finish(
204                invoke_json(upstream, target, request, limits.codec).await,
205                memory::responses_text,
206                index,
207                output.len(),
208                &id,
209            )?,
210        };
211        let (trace, memory) = memory::parse_output(&text, limits.codec)
212            .map_err(|e| MemoryError::at(e, index + 1, output.len(), Some(id.clone())))?;
213        let value = MemorySummarizeOutput::builder(trace, memory).build();
214        let bytes = codec::encode_json(&value, limits.codec).map_err(|e| {
215            MemoryError::at(
216                TransformError::new(TransformErrorKind::Limit, "memory.output", e.to_string()),
217                index + 1,
218                output.len(),
219                Some(id.clone()),
220            )
221        })?;
222        output_bytes = output_bytes
223            .checked_add(bytes.len() as u64)
224            .filter(|v| *v <= limits.max_bytes)
225            .ok_or_else(|| {
226                MemoryError::at(limit("max_bytes"), index + 1, output.len(), Some(id))
227            })?;
228        output.push(value);
229    }
230    Ok(output)
231}
232
233fn finish<T>(
234    result: Result<JsonInvocation<T>, TransformError>,
235    extract: fn(T) -> Result<String, TransformError>,
236    index: usize,
237    completed: usize,
238    id: &str,
239) -> Result<String, MemoryError> {
240    match result.map_err(|e| MemoryError::at(e, index + 1, completed, Some(id.into())))? {
241        JsonInvocation::Success(response) => extract(response.body)
242            .map_err(|e| MemoryError::at(e, index + 1, completed, Some(id.into()))),
243        JsonInvocation::Rejected(response) => Err(MemoryError {
244            attempted_calls: index + 1,
245            completed_traces: completed,
246            trace_id: Some(id.into()),
247            failure: MemoryFailure::Rejected(Box::new(response)),
248        }),
249    }
250}
251
252fn limit(field: &str) -> TransformError {
253    TransformError::new(
254        TransformErrorKind::Limit,
255        format!("memory.{field}"),
256        "memory operation bound exceeded",
257    )
258}