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}