Skip to main content

codex_convert_proxy/proxy/
context.rs

1//! Proxy context data structures.
2
3use std::time::Instant;
4
5use crate::convert::{ResponseRequestContext, StreamState};
6use crate::config::BackendInfo;
7use crate::types::chat_api::ChatMessage;
8
9/// Proxy context attached to each request session.
10#[derive(Debug)]
11pub struct ProxyContext {
12    /// Request start time for duration tracking.
13    pub start_time: Instant,
14    /// Collected request body bytes.
15    pub request_body: Vec<u8>,
16    /// Model name parsed from request.
17    pub model: Option<String>,
18    /// Selected backend for this request.
19    pub selected_backend: Option<BackendInfo>,
20    /// Provider name.
21    pub provider_name: Option<String>,
22    /// Stream state for SSE conversion (also used for non-streaming conversion context).
23    pub stream_state: Option<StreamState>,
24    /// Response body collected for conversion.
25    pub response_body: Vec<u8>,
26    /// Whether streaming is enabled.
27    pub is_streaming: bool,
28    /// Rewritten upstream path.
29    pub rewritten_path: Option<String>,
30    /// Whether this is a streaming response (for conversion tracking).
31    pub is_stream_response: bool,
32    /// Whether this is a conversion request (Responses API -> Chat API).
33    pub is_conversion_request: bool,
34    /// Offset in response_body that has been parsed (to avoid re-parsing events).
35    pub stream_body_parsed_offset: usize,
36    /// Request path after optional routing prefix stripping.
37    pub normalized_path: Option<String>,
38    /// Whether current upstream response should be converted as SSE stream.
39    pub should_convert_stream_response: bool,
40    /// Upstream status code captured in response_filter for diagnostics.
41    pub upstream_status: Option<u16>,
42    /// Upstream content-type captured in response_filter for diagnostics.
43    pub upstream_content_type: Option<String>,
44    /// Number of valid upstream chat stream chunks parsed.
45    pub stream_chunks_parsed: usize,
46    /// Conversation messages before upstream response (for follow-up turn storage).
47    pub pending_conversation_messages: Option<Vec<ChatMessage>>,
48    /// Effective instructions used for this request after previous_response expansion.
49    pub pending_instructions: Option<String>,
50}
51
52impl ProxyContext {
53    /// Create a new proxy context.
54    pub fn new() -> Self {
55        Self {
56            start_time: Instant::now(),
57            request_body: Vec::new(),
58            model: None,
59            selected_backend: None,
60            provider_name: None,
61            stream_state: None,
62            response_body: Vec::new(),
63            is_streaming: false,
64            rewritten_path: None,
65            is_stream_response: false,
66            is_conversion_request: false,
67            stream_body_parsed_offset: 0,
68            normalized_path: None,
69            should_convert_stream_response: false,
70            upstream_status: None,
71            upstream_content_type: None,
72            stream_chunks_parsed: 0,
73            pending_conversation_messages: None,
74            pending_instructions: None,
75        }
76    }
77
78    /// Parse model name and stream flag from request body.
79    /// Initializes StreamState for ALL conversion requests (both streaming and non-streaming).
80    /// StreamState holds ResponseRequestContext for protocol-aligned response generation.
81    pub fn init_from_request_body(&mut self) {
82        if self.model.is_some() {
83            return;
84        }
85        if let Ok(text) = std::str::from_utf8(&self.request_body)
86            && let Ok(json) = serde_json::from_str::<serde_json::Value>(text) {
87                if let Some(model) = json.get("model").and_then(|v| v.as_str()) {
88                    self.model = Some(model.to_string());
89                }
90                if let Some(stream) = json.get("stream").and_then(|v| v.as_bool()) {
91                    self.is_streaming = stream;
92                    if stream {
93                        self.is_stream_response = true;
94                    }
95                }
96
97                // Always initialize StreamState for conversion requests to hold context
98                // This is used for both streaming and non-streaming conversion
99                if self.is_conversion_request {
100                    let model = self.model.clone().unwrap_or_else(|| "unknown".to_string());
101                    // Get response_request_context from the stored request if available
102                    let context = self.stream_state
103                        .as_ref()
104                        .and_then(|s| s.request_context.clone());
105                    self.stream_state = Some(StreamState::new(
106                        format!("resp_{}", uuid::Uuid::new_v4()),
107                        model,
108                        context,
109                    ));
110                }
111            }
112    }
113
114    /// Set the response request context from a parsed ResponseRequest.
115    /// This should be called during request_body_filter processing.
116    pub fn set_response_request_context(&mut self, context: ResponseRequestContext) {
117        // If stream_state already exists, update its request_context
118        if let Some(ref mut state) = self.stream_state {
119            state.request_context = Some(context);
120        } else {
121            // Create stream_state with the context
122            let model = self.model.clone().unwrap_or_else(|| "unknown".to_string());
123            self.stream_state = Some(StreamState::new(
124                format!("resp_{}", uuid::Uuid::new_v4()),
125                model,
126                Some(context),
127            ));
128        }
129    }
130}
131
132impl Default for ProxyContext {
133    fn default() -> Self {
134        Self::new()
135    }
136}