1use std::collections::{HashMap, HashSet};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{LazyLock, Mutex};
13use std::time::Duration;
14
15use futures_util::{SinkExt as _, StreamExt as _};
16use serde::Serialize;
17use serde_json::{Map, Value, json};
18use tokio::sync::oneshot;
19use tokio::task::JoinHandle;
20use tokio::time::Instant;
21use tokio_tungstenite::WebSocketStream;
22use tokio_tungstenite::tungstenite::Message as Frame;
23use tokio_tungstenite::tungstenite::handshake::client::generate_key;
24use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
25use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
26use tokio_tungstenite::tungstenite::protocol::{CloseFrame, Role};
27use tokio_util::sync::CancellationToken;
28use yapi_types::sync::lock;
29
30use crate::http::{self, Headers};
31use crate::stream::{Transport, now_ms};
32
33type Socket = WebSocketStream<reqwest::Upgraded>;
34
35type Key = (String, String);
37
38const IDLE_TTL: Duration = Duration::from_secs(5 * 60);
39const MAX_AGE: Duration = Duration::from_secs(55 * 60);
40const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 15_000;
41const WEBSOCKET_ERROR: &str = "WebSocket error";
43
44enum Slot {
45 Busy,
47 Idle(Idle),
48}
49
50struct Idle {
52 stop: oneshot::Sender<Option<&'static str>>,
54 reader: JoinHandle<Option<Socket>>,
56 created: Instant,
57 continuation: Option<Continuation>,
58 generation: u64,
59}
60
61static SOCKETS: LazyLock<Mutex<HashMap<Key, Slot>>> = LazyLock::new(Default::default);
62static GENERATION: AtomicU64 = AtomicU64::new(0);
63static FALLBACK: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(Default::default);
66static STATS: LazyLock<Mutex<HashMap<String, DebugStats>>> = LazyLock::new(Default::default);
67
68#[derive(Clone, Debug, Default, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct DebugStats {
72 requests: u64,
73 connections_created: u64,
74 connections_reused: u64,
75 cached_context_requests: u64,
76 store_true_requests: u64,
77 full_context_requests: u64,
78 delta_requests: u64,
79 last_input_items: usize,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 last_delta_input_items: Option<usize>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 last_previous_response_id: Option<String>,
84 websocket_failures: u64,
85 sse_fallbacks: u64,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 websocket_fallback_active: Option<bool>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 last_web_socket_error: Option<String>,
90}
91
92fn with_stats(session: &str, update: impl FnOnce(&mut DebugStats)) {
93 update(lock(&STATS).entry(session.to_owned()).or_default());
94}
95
96pub fn debug_stats(session_id: &str) -> Option<DebugStats> {
99 lock(&STATS).get(session_id).cloned()
100}
101
102pub fn reset_debug_stats(session_id: Option<&str>) {
105 match session_id {
106 Some(session) => {
107 lock(&STATS).remove(session);
108 lock(&FALLBACK).remove(session);
109 }
110 None => {
111 lock(&STATS).clear();
112 lock(&FALLBACK).clear();
113 }
114 }
115}
116
117pub(super) fn fallback_active(session: Option<&str>) -> bool {
119 session.is_some_and(|session| lock(&FALLBACK).contains(session))
120}
121
122pub(super) fn count_request(session: &str, reused: bool, continues: bool, request: &Value) {
124 with_stats(session, |stats| {
125 stats.requests += 1;
126 if reused {
127 stats.connections_reused += 1;
128 } else {
129 stats.connections_created += 1;
130 }
131 if continues {
132 stats.cached_context_requests += 1;
133 }
134 if request["store"] == true {
135 stats.store_true_requests += 1;
136 }
137 let items = request["input"].as_array().map_or(0, Vec::len);
138 stats.last_input_items = items;
139 match request["previous_response_id"].as_str() {
140 Some(previous) => {
141 stats.delta_requests += 1;
142 stats.last_delta_input_items = Some(items);
143 stats.last_previous_response_id = Some(previous.to_owned());
144 }
145 None => {
146 stats.full_context_requests += 1;
147 stats.last_delta_input_items = None;
148 stats.last_previous_response_id = None;
149 }
150 }
151 });
152}
153
154pub(super) fn record_fallback(session: Option<&str>) {
156 if let Some(session) = session {
157 let active = fallback_active(Some(session));
158 with_stats(session, |stats| {
159 stats.sse_fallbacks += 1;
160 stats.websocket_fallback_active = Some(active);
161 });
162 }
163}
164
165pub(super) fn record_failure(session: Option<&str>, message: &str) {
167 if let Some(session) = session {
168 lock(&FALLBACK).insert(session.to_owned());
169 with_stats(session, |stats| {
170 stats.websocket_failures += 1;
171 stats.last_web_socket_error = Some(message.to_owned());
172 stats.websocket_fallback_active = Some(true);
173 });
174 }
175}
176
177pub fn close_sessions(session_id: Option<&str>) {
181 let mut sockets = lock(&SOCKETS);
182 let keys: Vec<Key> = sockets
183 .keys()
184 .filter(|(session, _)| session_id.is_none_or(|id| session == id))
185 .cloned()
186 .collect();
187 for key in keys {
188 if let Some(Slot::Idle(idle)) = sockets.remove(&key) {
189 let _ = idle.stop.send(Some("debug_close"));
190 }
191 }
192}
193
194#[derive(Debug)]
196pub(super) struct Failure {
197 pub(super) transport: bool,
200 name: &'static str,
202 pub(super) message: String,
203 code: Option<Value>,
205}
206
207impl Failure {
208 pub(super) fn error(message: impl Into<String>) -> Failure {
210 Failure {
211 transport: true,
212 name: "Error",
213 message: message.into(),
214 code: None,
215 }
216 }
217
218 pub(super) fn api(message: String, code: Option<&str>) -> Failure {
220 Failure {
221 transport: false,
222 name: "CodexApiError",
223 message,
224 code: code.map(Value::from),
225 }
226 }
227
228 pub(super) fn protocol(message: String) -> Failure {
230 Failure {
231 transport: false,
232 name: "CodexProtocolError",
233 message,
234 code: None,
235 }
236 }
237
238 fn closed(frame: Option<CloseFrame>) -> Failure {
239 let (code, reason) = frame.map_or((1005, String::new()), |frame| {
240 (u16::from(frame.code), frame.reason.to_string())
241 });
242 let reason = if reason.is_empty() && code == 1009 {
243 "message too big".to_owned()
244 } else {
245 reason
246 };
247 Failure {
248 transport: true,
249 name: "WebSocketCloseError",
250 message: format!("WebSocket closed {code} {reason}")
251 .trim_end()
252 .to_owned(),
253 code: Some(Value::from(code)),
254 }
255 }
256
257 pub(super) fn api_code(&self) -> Option<&str> {
259 self.code
260 .as_ref()
261 .and_then(Value::as_str)
262 .filter(|_| !self.transport)
263 }
264
265 pub(super) fn diagnostic(
269 &self,
270 transport: Transport,
271 events: bool,
272 request_bytes: usize,
273 ) -> Value {
274 let mut error = Map::new();
275 error.insert("name".into(), json!(self.name));
276 error.insert("message".into(), json!(self.message));
277 if let Some(code) = &self.code {
278 error.insert("code".into(), code.clone());
279 }
280 let mut details = Map::new();
281 details.insert("configuredTransport".into(), json!(transport));
282 if !events {
283 details.insert("fallbackTransport".into(), json!("sse"));
284 }
285 details.insert("eventsEmitted".into(), json!(events));
286 let phase = if events {
287 "after_message_stream_start"
288 } else {
289 "before_message_stream_start"
290 };
291 details.insert("phase".into(), json!(phase));
292 details.insert("requestBytes".into(), json!(request_bytes));
293 json!({
294 "type": "provider_transport_failure",
295 "timestamp": now_ms(),
296 "error": error,
297 "details": details,
298 })
299 }
300}
301
302pub(super) struct Continuation {
304 pub(super) body: Value,
306 pub(super) response_id: String,
307 pub(super) items: Vec<Value>,
309}
310
311pub(super) fn continued(body: &Value, continuation: &mut Option<Continuation>) -> Value {
316 let Some(last) = continuation else {
317 return body.clone();
318 };
319 match delta(body, last) {
320 Some(input) if !last.response_id.is_empty() => {
321 let mut next = body.clone();
322 next["previous_response_id"] = json!(last.response_id);
323 next["input"] = Value::Array(input);
324 next
325 }
326 _ => {
327 *continuation = None;
328 body.clone()
329 }
330 }
331}
332
333fn delta(body: &Value, last: &Continuation) -> Option<Vec<Value>> {
335 let rest = |body: &Value| {
336 let mut rest = body.as_object()?.clone();
337 rest.shift_remove("input");
338 rest.shift_remove("previous_response_id");
339 Some(yapi_types::json::stringify(&rest))
340 };
341 if rest(body)? != rest(&last.body)? {
342 return None;
343 }
344 let input = |body: &Value| body["input"].as_array().cloned().unwrap_or_default();
345 let current = input(body);
346 let mut baseline = input(&last.body);
347 baseline.extend(last.items.iter().cloned());
348 let same =
349 |(a, b): (&Value, &Value)| yapi_types::json::stringify(a) == yapi_types::json::stringify(b);
350 if current.len() < baseline.len() || !current.iter().zip(&baseline).all(same) {
351 return None;
352 }
353 Some(current[baseline.len()..].to_vec())
354}
355
356pub(super) fn frame(body: &Value) -> String {
358 let mut frame = Map::new();
359 frame.insert("type".into(), json!("response.create"));
360 if let Some(fields) = body.as_object() {
361 frame.extend(fields.clone());
362 }
363 yapi_types::json::stringify(&frame)
364}
365
366pub(super) struct Lease {
368 socket: Socket,
369 key: BusySlot,
371 created: Instant,
372 pub(super) reused: bool,
374 pub(super) continuation: Option<Continuation>,
376}
377
378pub(super) async fn acquire(
382 url: &str,
383 headers: &Headers,
384 key: Option<Key>,
385 cancel: &CancellationToken,
386 connect_timeout_ms: Option<u64>,
387) -> Result<Lease, Failure> {
388 let open = || connect(url, headers, cancel, connect_timeout_ms);
389 let lease = |socket, key, created| Lease {
390 socket,
391 key: BusySlot(key),
392 created,
393 reused: false,
394 continuation: None,
395 };
396 let Some(key) = key else {
397 return Ok(lease(open().await?, None, Instant::now()));
398 };
399 let found = {
400 let mut sockets = lock(&SOCKETS);
401 let found = sockets.remove(&key);
402 if found.is_some() {
403 sockets.insert(key.clone(), Slot::Busy);
404 }
405 found
406 };
407 match found {
408 Some(Slot::Busy) => return Ok(lease(open().await?, None, Instant::now())),
409 Some(Slot::Idle(idle)) => {
410 let _ = idle.stop.send(None);
411 if let Ok(Some(socket)) = idle.reader.await {
412 if idle.created.elapsed() < MAX_AGE {
413 return Ok(Lease {
414 reused: true,
415 continuation: idle.continuation,
416 ..lease(socket, Some(key), idle.created)
417 });
418 }
419 close(socket, "connection_age_limit");
420 }
421 }
422 None => {}
423 }
424 let socket = match open().await {
425 Ok(socket) => socket,
426 Err(failure) => {
427 remove_busy(&key);
428 return Err(failure);
429 }
430 };
431 lock(&SOCKETS).insert(key.clone(), Slot::Busy);
432 Ok(lease(socket, Some(key), Instant::now()))
433}
434
435struct BusySlot(Option<Key>);
439
440impl Drop for BusySlot {
441 fn drop(&mut self) {
442 if let Some(key) = &self.0 {
443 remove_busy(key);
444 }
445 }
446}
447
448fn remove_busy(key: &Key) {
450 let mut sockets = lock(&SOCKETS);
451 if matches!(sockets.get(key), Some(Slot::Busy)) {
452 sockets.remove(key);
453 }
454}
455
456impl Lease {
457 pub(super) fn cached(&self) -> bool {
459 self.key.0.is_some()
460 }
461
462 pub(super) async fn send(&mut self, text: String) -> Result<(), Failure> {
464 self.socket
465 .send(Frame::text(text))
466 .await
467 .map_err(|_| Failure::error(WEBSOCKET_ERROR))
468 }
469
470 pub(super) async fn next(
474 &mut self,
475 cancel: &CancellationToken,
476 idle: Option<Duration>,
477 ) -> Result<String, Failure> {
478 let deadline = idle.map(|idle| (idle, tokio::time::Instant::now() + idle));
479 loop {
480 let read = self.socket.next();
481 let frame = match deadline {
482 Some((idle, at)) => cancel
483 .run_until_cancelled(tokio::time::timeout_at(at, read))
484 .await
485 .map(|frame| {
486 frame.map_err(|_| {
487 Failure::error(format!(
488 "WebSocket idle timeout after {}ms",
489 idle.as_millis()
490 ))
491 })
492 }),
493 None => cancel.run_until_cancelled(read).await.map(Ok),
494 };
495 let frame =
496 frame.unwrap_or_else(|| Err(Failure::error(http::ABORTED_DURING_STREAM)))?;
497 match frame {
499 Some(Ok(Frame::Text(text))) if !text.is_empty() => return Ok(text.to_string()),
500 Some(Ok(Frame::Binary(bytes))) if !bytes.is_empty() => {
501 return Ok(String::from_utf8_lossy(&bytes).into_owned());
502 }
503 Some(Ok(Frame::Close(frame))) => return Err(Failure::closed(frame)),
504 Some(Ok(_)) => {}
505 Some(Err(_)) | None => return Err(Failure::error(WEBSOCKET_ERROR)),
506 }
507 }
508 }
509
510 pub(super) fn release(mut self, keep: bool) {
514 let Some(key) = self.key.0.take() else {
515 return close(self.socket, "done");
516 };
517 let mut sockets = lock(&SOCKETS);
518 if !matches!(sockets.get(&key), Some(Slot::Busy)) {
519 return close(self.socket, "done");
520 }
521 if !keep {
522 sockets.remove(&key);
523 return close(self.socket, "done");
524 }
525 let generation = GENERATION.fetch_add(1, Ordering::Relaxed);
526 let (stop, stopped) = oneshot::channel();
527 let reader = tokio::spawn(read_idle(self.socket, stopped, key.clone(), generation));
528 sockets.insert(
529 key,
530 Slot::Idle(Idle {
531 stop,
532 reader,
533 created: self.created,
534 continuation: self.continuation,
535 generation,
536 }),
537 );
538 }
539}
540
541async fn read_idle(
545 mut socket: Socket,
546 mut stopped: oneshot::Receiver<Option<&'static str>>,
547 key: Key,
548 generation: u64,
549) -> Option<Socket> {
550 let expiry = tokio::time::sleep(IDLE_TTL);
551 tokio::pin!(expiry);
552 let reason = loop {
553 tokio::select! {
554 stop = &mut stopped => match stop {
555 Ok(None) => return Some(socket),
556 Ok(Some(reason)) => break Some(reason),
557 Err(_) => break Some("done"),
558 },
559 () = &mut expiry => break Some("idle_timeout"),
560 frame = socket.next() => match frame {
561 Some(Ok(Frame::Close(_)) | Err(_)) | None => break None,
562 Some(Ok(_)) => {}
564 },
565 }
566 };
567 {
568 let mut sockets = lock(&SOCKETS);
569 if matches!(sockets.get(&key), Some(Slot::Idle(idle)) if idle.generation == generation) {
570 sockets.remove(&key);
571 }
572 }
573 if let Some(reason) = reason {
574 close(socket, reason);
575 }
576 None
577}
578
579fn close(mut socket: Socket, reason: &'static str) {
581 tokio::spawn(async move {
582 let frame = CloseFrame {
583 code: CloseCode::Normal,
584 reason: reason.into(),
585 };
586 let _ = socket.close(Some(frame)).await;
587 });
588}
589
590async fn connect(
593 url: &str,
594 headers: &Headers,
595 cancel: &CancellationToken,
596 timeout_ms: Option<u64>,
597) -> Result<Socket, Failure> {
598 let key = generate_key();
599 let request = headers
600 .apply(http::upgrade_client().get(url))
601 .header("connection", "Upgrade")
602 .header("upgrade", "websocket")
603 .header("sec-websocket-version", "13")
604 .header("sec-websocket-key", &key);
605 let open = async {
606 let failed = |_| Failure::error(WEBSOCKET_ERROR);
607 let response = request.send().await.map_err(failed)?;
608 let accepted = response
609 .headers()
610 .get("sec-websocket-accept")
611 .is_some_and(|accept| {
612 accept.as_bytes() == derive_accept_key(key.as_bytes()).as_bytes()
613 });
614 if response.status() != reqwest::StatusCode::SWITCHING_PROTOCOLS || !accepted {
615 return Err(Failure::error(WEBSOCKET_ERROR));
616 }
617 let upgraded = response.upgrade().await.map_err(failed)?;
618 Ok(WebSocketStream::from_raw_socket(upgraded, Role::Client, None).await)
619 };
620 let timeout_ms = timeout_ms.unwrap_or(DEFAULT_CONNECT_TIMEOUT_MS);
621 let open = async {
622 if timeout_ms == 0 {
623 return open.await;
624 }
625 tokio::time::timeout(Duration::from_millis(timeout_ms), open)
626 .await
627 .unwrap_or_else(|_| {
628 Err(Failure::error(format!(
629 "WebSocket connect timeout after {timeout_ms}ms"
630 )))
631 })
632 };
633 cancel
634 .run_until_cancelled(open)
635 .await
636 .unwrap_or_else(|| Err(Failure::error(http::ABORTED_DURING_STREAM)))
637}