gproxy_protocol/adapt/generate/stream/
websocket.rs1use super::super::GenerationStateAccess;
5use super::{StreamChunk, StreamInvocation, bridge::StreamBridge};
6use crate::{
7 HttpBody, WireRequest, WireResponse,
8 adapt::responses_ws as ws,
9 capability::{StateStore, Upstream},
10 transform::TransformError,
11 wire::{
12 DeclaredFields,
13 openai::responses::{
14 self as r,
15 stream::StreamEvent,
16 websocket::{ClientEvent, RequestMessage},
17 },
18 },
19};
20
21pub struct GenerationWsSession {
22 native: ws::ResponsesWsSession,
23 binding: super::binding::StateBinding,
25}
26
27#[allow(clippy::large_enum_variant)]
28pub enum GenerationWsConnect {
29 Connected {
30 handshake: WireResponse<()>,
31 session: GenerationWsSession,
32 },
33 Rejected(WireResponse<HttpBody>),
34}
35
36impl GenerationWsSession {
37 pub fn native(&self) -> &ws::ResponsesWsSession {
38 &self.native
39 }
40}
41
42pub async fn connect<U: Upstream, S: StateStore>(
43 upstream: &U,
44 target: &U::Target,
45 request: WireRequest<()>,
46 limits: ws::ResponsesWsLimits,
47 state: &GenerationStateAccess<'_, S>,
48) -> Result<GenerationWsConnect, ws::ResponsesWsConnectError> {
49 let binding = super::binding::StateBinding::new(state);
50 match ws::connect(upstream, target, request, limits).await? {
51 ws::ResponsesWsConnect::Connected { handshake, session } => {
52 Ok(GenerationWsConnect::Connected {
53 handshake,
54 session: GenerationWsSession {
55 native: session,
56 binding,
57 },
58 })
59 }
60 ws::ResponsesWsConnect::Rejected(response) => Ok(GenerationWsConnect::Rejected(response)),
61 }
62}
63
64pub struct GenerationWsTurn<'a, B: StreamBridge<NativeEvent = StreamEvent>> {
67 invocation: &'a mut StreamInvocation<B>,
68 turn: Option<ws::ResponsesWsTurn<'a>>,
69 failure: Option<StreamEvent>,
70 websocket_failure: Option<r::websocket::ErrorMessage>,
71}
72
73impl<B: StreamBridge<NativeEvent = StreamEvent>> GenerationWsTurn<'_, B> {
74 pub async fn next<S: StateStore>(
75 &mut self,
76 state: &GenerationStateAccess<'_, S>,
77 ) -> Result<Option<StreamChunk<B::ClientEvent>>, TransformError> {
78 let Some(turn) = &mut self.turn else {
79 return Err(super::invalid("WebSocket generation turn failed"));
80 };
81 let result = self.invocation.next_inner(state, Some(turn)).await;
82 if result.is_err() && self.invocation.failed {
83 self.failure = turn.failure_event().cloned();
84 self.websocket_failure = turn.websocket_failure().cloned();
85 self.turn.take();
86 }
87 result
88 }
89 pub async fn collect<S: StateStore>(
90 &mut self,
91 state: &GenerationStateAccess<'_, S>,
92 ) -> Result<super::invoke::ClientFull<B>, TransformError> {
93 while self.next(state).await?.is_some() {}
94 let result = self
95 .invocation
96 .client_result()
97 .ok_or_else(|| super::missing("WebSocket client result is not durably committed"))?
98 .clone();
99 crate::codec::encode_json(&result, self.invocation.settings.codec)
100 .map_err(super::codec_error)?;
101 Ok(result)
102 }
103 pub fn invocation(&self) -> &StreamInvocation<B> {
104 self.invocation
105 }
106 pub fn failure_event(&self) -> Option<&StreamEvent> {
107 self.failure
108 .as_ref()
109 .or_else(|| self.turn.as_ref().and_then(|turn| turn.failure_event()))
110 }
111 pub fn websocket_failure(&self) -> Option<&r::websocket::ErrorMessage> {
112 self.websocket_failure
113 .as_ref()
114 .or_else(|| self.turn.as_ref().and_then(|turn| turn.websocket_failure()))
115 }
116}
117
118impl<B: StreamBridge<NativeEvent = StreamEvent, NativeRequest = r::GenerateContentRequestBody>>
119 StreamInvocation<B>
120{
121 pub async fn start_websocket<'a, S: StateStore>(
124 &'a mut self,
125 session: &'a mut GenerationWsSession,
126 state: &GenerationStateAccess<'_, S>,
127 ) -> Result<GenerationWsTurn<'a, B>, TransformError> {
128 self.start_websocket_on_lane(session, None, state).await
129 }
130 pub async fn start_websocket_on_lane<'a, S: StateStore>(
131 &'a mut self,
132 session: &'a mut GenerationWsSession,
133 lane: Option<String>,
134 state: &GenerationStateAccess<'_, S>,
135 ) -> Result<GenerationWsTurn<'a, B>, TransformError> {
136 self.binding.check(state)?;
137 session.binding.check(state)?;
138 if self.sent || self.failed {
139 return Err(super::conflict("invocation already sent or failed"));
140 }
141 if self
142 .websocket_lane
143 .as_ref()
144 .is_some_and(|known| known != &lane)
145 {
146 return Err(super::conflict("WebSocket turn lane changed"));
147 }
148 let mut request = self.target.clone().into_declared();
149 request.stream = None;
152 self.websocket_lane = Some(lane.clone());
153 let turn = session.native.turn_message(RequestMessage {
154 stream_id: lane,
155 generate: None,
156 event: ClientEvent::ResponseCreate(request),
157 })?;
158 self.sent = true;
159 Ok(GenerationWsTurn {
160 invocation: self,
161 turn: Some(turn),
162 failure: None,
163 websocket_failure: None,
164 })
165 }
166}
167
168impl<B: StreamBridge<ClientEvent = StreamEvent>> StreamInvocation<B> {
169 pub fn responses_websocket_output(&mut self) -> Result<(), TransformError> {
172 self.responses_websocket_output_on_lane(None)
173 }
174 pub fn responses_websocket_output_on_lane(
175 &mut self,
176 lane: Option<String>,
177 ) -> Result<(), TransformError> {
178 if self.sent || self.failed {
179 return Err(super::conflict("select WebSocket output before send"));
180 }
181 self.encoder = super::output::Encoder::WebSocket {
182 total: 0,
183 events: 0,
184 limits: self.settings.codec,
185 lane,
186 };
187 Ok(())
188 }
189}
190
191pub fn decode_request(
193 bytes: &[u8],
194 limits: crate::codec::CodecLimits,
195) -> Result<r::GenerateContentRequestBody, TransformError> {
196 let message = decode_message(bytes, limits)?;
197 if message.stream_id.is_some() {
198 return Err(TransformError::shape(
199 "responses.websocket.stream_id",
200 "use decode_message and preserve the explicit output lane",
201 ));
202 }
203 let ClientEvent::ResponseCreate(request) = message.event else {
204 return Err(TransformError::unsupported(
205 "responses.websocket.control",
206 "steering and injection have no equivalent cross-protocol generation operation",
207 ));
208 };
209 Ok(request)
210}
211
212pub fn decode_message(
215 bytes: &[u8],
216 limits: crate::codec::CodecLimits,
217) -> Result<RequestMessage, TransformError> {
218 let message = decode_session_message(bytes, limits)?;
219 if message.generate == Some(false) {
220 return Err(TransformError::unsupported(
221 "responses.websocket.generate",
222 "prefill-only warmup requires a Responses session driver",
223 ));
224 }
225 if !matches!(message.event, ClientEvent::ResponseCreate(_)) {
226 return Err(TransformError::unsupported(
227 "responses.websocket.control",
228 "control messages require a Responses session driver",
229 ));
230 }
231 Ok(message)
232}
233
234pub fn decode_session_message(
237 bytes: &[u8],
238 limits: crate::codec::CodecLimits,
239) -> Result<RequestMessage, TransformError> {
240 let mut message = crate::codec::decode_json::<RequestMessage>(bytes, limits)
241 .map_err(|error| {
242 let kind = if error.kind() == crate::codec::CodecErrorKind::Limit {
243 crate::transform::TransformErrorKind::Limit
244 } else {
245 crate::transform::TransformErrorKind::InvalidInput
246 };
247 TransformError::with_source(
248 kind,
249 "responses.websocket.request",
250 error.to_string(),
251 error,
252 )
253 })?
254 .into_declared();
255
256 if let Some(lane) = &message.stream_id
257 && (lane.is_empty()
258 || lane.len() > 256
259 || !lane
260 .bytes()
261 .all(|b| b.is_ascii_alphanumeric() || b"_.-".contains(&b)))
262 {
263 return Err(TransformError::shape(
264 "responses.websocket.stream_id",
265 "invalid lane",
266 ));
267 }
268 let ClientEvent::ResponseCreate(request) = &mut message.event else {
269 return Ok(message);
270 };
271 if request.background.flatten() == Some(true) {
272 return Err(TransformError::unsupported(
273 "responses.websocket.background",
274 "WebSocket mode has no background generation control",
275 ));
276 }
277 request.stream = None;
278 request.background = None;
279 Ok(message)
280}
281
282mod image_resources;