Skip to main content

codex_convert_proxy/proxy/
context.rs

1//! Proxy context data structures.
2//!
3//! The per-request `ProxyContext` composes several single-responsibility
4//! sub-structs so each cluster of related fields can be reasoned about (and
5//! initialised) independently:
6//!
7//! - [`RouteInfo`] — backend selection + path rewriting (populated in
8//!   `request_filter`).
9//! - [`ConversionFlags`] — boolean state machine that drives whether and how
10//!   bodies are converted.
11//! - [`ConversionBuffers`] — buffered request/response bodies + stream parse
12//!   cursor.
13//! - [`UpstreamDiagnostics`] — observed upstream status / content-type / chunk
14//!   count, kept for logging and bypass decisions.
15//! - [`FollowUpContext`] — conversation messages we'll persist after the
16//!   upstream completes (for `previous_response_id` expansion).
17//!
18//! The root keeps only fields that don't fit any cluster: `start_time`,
19//! `model`, and the streaming `StreamState`.
20
21use std::time::Instant;
22
23use crate::convert::{ResponseRequestContext, StreamState};
24use crate::config::BackendInfo;
25use crate::types::chat_api::ChatMessage;
26use crate::types::response_api::ResponseRequest;
27
28/// Backend selection and request-path information.
29#[derive(Debug, Default)]
30pub struct RouteInfo {
31    /// Selected backend connection info.
32    pub selected_backend: Option<BackendInfo>,
33    /// Backend / provider name (cached for diagnostics after the backend
34    /// reference is no longer convenient to hold).
35    pub provider_name: Option<String>,
36    /// Request path after optional `path_prefix` stripping.
37    pub normalized_path: Option<String>,
38    /// Rewritten upstream path (includes backend's base_path).
39    pub rewritten_path: Option<String>,
40}
41
42/// Boolean state machine that records what kind of conversion is in flight.
43#[derive(Debug, Default)]
44pub struct ConversionFlags {
45    /// True if the original request targets `/v1/responses` (or `/responses`)
46    /// and must be converted to a Chat-API request.
47    pub is_conversion_request: bool,
48    /// True if the client requested streaming (`stream: true`).
49    pub is_streaming: bool,
50    /// True if the upstream is producing a streaming response. Usually equal
51    /// to `is_streaming` but retained as a separate flag for clarity around
52    /// upstream content-type detection.
53    pub is_stream_response: bool,
54    /// True only after `response_filter` confirmed the upstream is producing
55    /// SSE we should convert. Drives `response_body_filter`'s streaming path.
56    pub should_convert_stream_response: bool,
57}
58
59/// Buffered request/response bodies and the stream-parse cursor.
60#[derive(Debug, Default)]
61pub struct ConversionBuffers {
62    /// Collected request body bytes (cleared after conversion).
63    pub request_body: Vec<u8>,
64    /// Collected response body bytes (for non-streaming conversion + stream
65    /// frame accumulation).
66    pub response_body: Vec<u8>,
67    /// Offset in `response_body` that has already been parsed, so SSE event
68    /// reparsing across chunks is bounded.
69    pub stream_body_parsed_offset: usize,
70}
71
72/// Upstream observations for diagnostics + bypass decisions.
73#[derive(Debug, Default)]
74pub struct UpstreamDiagnostics {
75    /// Status code captured in `response_filter`.
76    pub upstream_status: Option<u16>,
77    /// `content-type` captured in `response_filter`.
78    pub upstream_content_type: Option<String>,
79    /// Count of valid upstream stream chunks parsed (drives "skip
80    /// response.completed" bypass when zero).
81    pub stream_chunks_parsed: usize,
82}
83
84/// Information needed to persist a completed turn for subsequent
85/// `previous_response_id` lookups.
86#[derive(Debug, Default)]
87pub struct FollowUpContext {
88    /// Conversation messages collected before the upstream response (so the
89    /// assistant reply can be appended to them and stored).
90    pub pending_conversation_messages: Option<Vec<ChatMessage>>,
91    /// Effective instructions used for this turn (after history expansion).
92    pub pending_instructions: Option<String>,
93}
94
95/// Proxy context attached to each request session.
96#[derive(Debug)]
97pub struct ProxyContext {
98    /// Request start time for duration tracking.
99    pub start_time: Instant,
100    /// Model name parsed from request.
101    pub model: Option<String>,
102    /// Stream state for SSE conversion (also used as a carrier for
103    /// `ResponseRequestContext` in non-streaming conversions; this dual use is
104    /// scheduled to be decoupled in a follow-up commit).
105    pub stream_state: Option<StreamState>,
106
107    pub route: RouteInfo,
108    pub flags: ConversionFlags,
109    pub buffers: ConversionBuffers,
110    pub diagnostics: UpstreamDiagnostics,
111    pub follow_up: FollowUpContext,
112}
113
114impl ProxyContext {
115    /// Create a new proxy context.
116    pub fn new() -> Self {
117        Self {
118            start_time: Instant::now(),
119            model: None,
120            stream_state: None,
121            route: RouteInfo::default(),
122            flags: ConversionFlags::default(),
123            buffers: ConversionBuffers::default(),
124            diagnostics: UpstreamDiagnostics::default(),
125            follow_up: FollowUpContext::default(),
126        }
127    }
128
129    /// Populate `model` + streaming flags from a parsed `ResponseRequest`.
130    ///
131    /// Conversion path: callers already deserialize the body as `ResponseRequest`
132    /// to perform the conversion, so we accept the parsed struct directly to
133    /// avoid re-parsing the JSON.
134    pub fn init_from_response_request(&mut self, req: &ResponseRequest) {
135        if self.model.is_none() {
136            self.model = Some(req.model.clone());
137        }
138        self.flags.is_streaming = req.stream;
139        if req.stream {
140            self.flags.is_stream_response = true;
141        }
142
143        if self.flags.is_conversion_request {
144            let model = self.model.clone().unwrap_or_else(|| "unknown".to_string());
145            let context = self
146                .stream_state
147                .as_ref()
148                .and_then(|s| s.request_context.clone());
149            self.stream_state = Some(StreamState::new(
150                format!("resp_{}", uuid::Uuid::new_v4()),
151                model,
152                context,
153            ));
154        }
155    }
156
157    /// Pass-through path: extract just `model` + `stream` from a serde_json::Value.
158    ///
159    /// Used when the body is not a Responses API request (e.g., direct Chat
160    /// Completions pass-through), so a full typed parse would be wrong.
161    pub fn init_from_passthrough_json(&mut self, json: &serde_json::Value) {
162        if self.model.is_none()
163            && let Some(model) = json.get("model").and_then(|v| v.as_str())
164        {
165            self.model = Some(model.to_string());
166        }
167        if let Some(stream) = json.get("stream").and_then(|v| v.as_bool()) {
168            self.flags.is_streaming = stream;
169            if stream {
170                self.flags.is_stream_response = true;
171            }
172        }
173    }
174
175    /// Set the response request context from a parsed ResponseRequest.
176    /// This should be called during request_body_filter processing.
177    pub fn set_response_request_context(&mut self, context: ResponseRequestContext) {
178        // If stream_state already exists, update its request_context
179        if let Some(ref mut state) = self.stream_state {
180            state.request_context = Some(context);
181        } else {
182            // Create stream_state with the context
183            let model = self.model.clone().unwrap_or_else(|| "unknown".to_string());
184            self.stream_state = Some(StreamState::new(
185                format!("resp_{}", uuid::Uuid::new_v4()),
186                model,
187                Some(context),
188            ));
189        }
190    }
191}
192
193impl Default for ProxyContext {
194    fn default() -> Self {
195        Self::new()
196    }
197}