Skip to main content

github_copilot_sdk/
copilot_request_handler.rs

1//! Connection-level interception of the model-layer HTTP and WebSocket traffic
2//! the runtime issues — for both CAPI and BYOK sessions.
3//!
4//! When [`ClientOptions::request_handler`](crate::ClientOptions::request_handler)
5//! is set, the SDK registers itself as the runtime's request handler on
6//! [`Client::start`](crate::Client::start). From then on, whenever the runtime
7//! would issue a model-layer request (inference, `/models`, `/policy`, …) it
8//! asks the registered [`CopilotRequestHandler`] to service it instead of making
9//! the call itself.
10//!
11//! [`CopilotRequestHandler`] is the single seam consumers implement: one HTTP
12//! send method and one WebSocket factory, each defaulting to transparent
13//! pass-through to the real upstream. Override
14//! [`send_request`](CopilotRequestHandler::send_request) to mutate / replace HTTP
15//! requests, or [`open_websocket`](CopilotRequestHandler::open_websocket) to
16//! mutate the handshake or return a custom [`CopilotWebSocketHandler`].
17//!
18//! # Cancellation
19//!
20//! [`CopilotRequestContext::cancel`] fires when the runtime cancels the
21//! in-flight request (for example because the agent turn was aborted). Forward
22//! it to the upstream call so it is torn down too, and stop writing the response.
23
24use std::collections::HashMap;
25use std::pin::Pin;
26use std::sync::{Arc, LazyLock, OnceLock, Weak};
27
28use async_trait::async_trait;
29use base64::Engine;
30use bytes::Bytes;
31use futures_util::{SinkExt, Stream, StreamExt};
32use http::HeaderMap;
33use http::header::{HeaderName, HeaderValue};
34use parking_lot::Mutex;
35use tokio::net::TcpStream;
36use tokio::sync::{Mutex as AsyncMutex, mpsc};
37use tokio_tungstenite::tungstenite::Message;
38use tokio_tungstenite::tungstenite::client::IntoClientRequest;
39use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async};
40use tokio_util::sync::CancellationToken;
41use tracing::warn;
42
43use self::http_response_reader::HttpResponseReader;
44use crate::generated::api_types::{
45    LlmInferenceHttpRequestChunkRequest, LlmInferenceHttpRequestStartRequest,
46    LlmInferenceHttpRequestStartTransport, LlmInferenceHttpResponseChunkError,
47    LlmInferenceHttpResponseChunkRequest, LlmInferenceHttpResponseStartRequest,
48};
49use crate::{
50    Client, ClientInner, JsonRpcRequest, JsonRpcResponse, RequestId, SessionId, error_codes,
51};
52
53mod http_response_reader;
54
55const METHOD_HTTP_REQUEST_START: &str = "llmInference.httpRequestStart";
56const METHOD_HTTP_REQUEST_CHUNK: &str = "llmInference.httpRequestChunk";
57
58/// Transport the runtime would otherwise use for an intercepted request.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum CopilotRequestTransport {
61    /// Plain HTTP or SSE. Each response body frame is an opaque byte range.
62    #[default]
63    Http,
64    /// Full-duplex WebSocket. Each request/response body frame maps to exactly
65    /// one WebSocket message.
66    WebSocket,
67}
68
69impl CopilotRequestTransport {
70    fn from_wire(value: Option<LlmInferenceHttpRequestStartTransport>) -> Self {
71        match value {
72            Some(LlmInferenceHttpRequestStartTransport::Websocket) => Self::WebSocket,
73            _ => Self::Http,
74        }
75    }
76}
77
78/// Error returned by a [`CopilotRequestHandler`] hook or the response stream.
79#[derive(Debug)]
80#[non_exhaustive]
81pub enum CopilotRequestError {
82    /// The response was used after the RPC connection to the runtime closed.
83    ConnectionClosed,
84
85    /// The response state machine was violated (for example `start` called
86    /// twice, or a write before `start`).
87    InvalidState(String),
88
89    /// An upstream transport failure while forwarding the request.
90    Upstream(String),
91
92    /// A failure surfaced by the consumer's own handler.
93    Handler(String),
94
95    /// An RPC error talking to the runtime.
96    Rpc(crate::Error),
97}
98
99impl CopilotRequestError {
100    /// Construct a handler-level error from a message — the idiomatic way for a
101    /// consumer to fail an intercepted request.
102    pub fn message(message: impl Into<String>) -> Self {
103        Self::Handler(message.into())
104    }
105}
106
107impl std::fmt::Display for CopilotRequestError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::ConnectionClosed => {
111                f.write_str("Copilot request response used after RPC connection closed")
112            }
113            Self::InvalidState(message) | Self::Upstream(message) | Self::Handler(message) => {
114                f.write_str(message)
115            }
116            Self::Rpc(err) => write!(f, "{err}"),
117        }
118    }
119}
120
121impl std::error::Error for CopilotRequestError {
122    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
123        match self {
124            Self::Rpc(err) => Some(err),
125            _ => None,
126        }
127    }
128}
129
130impl From<crate::Error> for CopilotRequestError {
131    fn from(err: crate::Error) -> Self {
132        Self::Rpc(err)
133    }
134}
135
136/// Context describing an intercepted request, shared by the HTTP and WebSocket
137/// seams.
138#[derive(Clone)]
139#[non_exhaustive]
140pub struct CopilotRequestContext {
141    /// Opaque runtime-minted request id, stable across the request lifecycle.
142    pub request_id: String,
143    /// Id of the runtime session that triggered this request, or `None` when it
144    /// was issued outside any session (for example the startup model catalog).
145    pub session_id: Option<String>,
146    /// Stable per-agent-instance id for the agent trajectory that issued this request.
147    pub agent_id: Option<String>,
148    /// Id of the parent agent when this request was issued by a subagent.
149    pub parent_agent_id: Option<String>,
150    /// Runtime classification for the interaction that produced this request.
151    pub interaction_type: Option<String>,
152    /// Transport the runtime would otherwise use.
153    pub transport: CopilotRequestTransport,
154    /// Absolute request URL.
155    pub url: String,
156    /// Request headers, multi-valued.
157    pub headers: HeaderMap,
158    /// Fires when the runtime cancels this in-flight request.
159    pub cancel: CancellationToken,
160}
161
162/// Streaming response body: a sequence of byte chunks or a terminal error.
163///
164/// HTTP bytes are forwarded in order, but chunk boundaries are not preserved.
165/// The SDK reads ahead while awaiting runtime acknowledgements, using at most
166/// 64 KiB of raw-byte forwarding buffers plus one current source chunk. This
167/// excludes storage inside the source stream and RPC serialization. A source
168/// chunk (including its shared backing allocation) is not size-limited by this API.
169/// Bytes available after an acknowledgement are flushed without waiting for
170/// more input. WebSocket message boundaries are preserved separately.
171pub type CopilotHttpResponseBody =
172    Pin<Box<dyn Stream<Item = Result<Bytes, CopilotRequestError>> + Send>>;
173
174/// A buffered HTTP request handed to [`CopilotRequestHandler::send_request`].
175#[non_exhaustive]
176pub struct CopilotHttpRequest {
177    /// HTTP method (`GET`, `POST`, …).
178    pub method: String,
179    /// Absolute request URL.
180    pub url: String,
181    /// Request headers.
182    pub headers: HeaderMap,
183    /// Fully-buffered request body.
184    pub body: Vec<u8>,
185    /// Fires when the runtime cancels the request.
186    pub cancel: CancellationToken,
187}
188
189/// A streaming HTTP response returned by [`CopilotRequestHandler::send_request`].
190#[non_exhaustive]
191pub struct CopilotHttpResponse {
192    /// HTTP status code.
193    pub status: u16,
194    /// Optional status reason phrase.
195    pub status_text: Option<String>,
196    /// Response headers.
197    pub headers: HeaderMap,
198    /// Streaming response body.
199    pub body: CopilotHttpResponseBody,
200}
201
202impl CopilotHttpResponse {
203    /// Build a response with the given parts.
204    pub fn new(
205        status: u16,
206        status_text: Option<String>,
207        headers: HeaderMap,
208        body: CopilotHttpResponseBody,
209    ) -> Self {
210        Self {
211            status,
212            status_text,
213            headers,
214            body,
215        }
216    }
217}
218
219/// A single WebSocket message flowing through a [`CopilotWebSocketHandler`].
220#[derive(Clone)]
221pub struct CopilotWebSocketMessage {
222    /// Message payload.
223    pub data: Vec<u8>,
224    /// Whether the payload is a binary frame (`true`) or a text frame (`false`).
225    pub binary: bool,
226}
227
228impl CopilotWebSocketMessage {
229    /// A UTF-8 text message. Binary messages are constructed directly via the
230    /// public `data` / `binary` fields.
231    pub fn from_text(data: impl Into<String>) -> Self {
232        Self {
233            data: data.into().into_bytes(),
234            binary: false,
235        }
236    }
237}
238
239/// The runtime-facing side of a WebSocket: a [`CopilotWebSocketHandler`] writes
240/// upstream→runtime messages here.
241#[derive(Clone)]
242pub struct CopilotWebSocketResponse {
243    exchange: Arc<CopilotRequestExchange>,
244}
245
246impl CopilotWebSocketResponse {
247    fn new(exchange: Arc<CopilotRequestExchange>) -> Self {
248        Self { exchange }
249    }
250
251    /// Forward one upstream message to the runtime.
252    pub async fn send_message(
253        &self,
254        message: CopilotWebSocketMessage,
255    ) -> Result<(), CopilotRequestError> {
256        self.exchange.ensure_ws_started().await?;
257        if message.binary {
258            self.exchange.write_binary(&message.data).await
259        } else {
260            let text = String::from_utf8_lossy(&message.data);
261            self.exchange.write_text(&text).await
262        }
263    }
264
265    /// End the runtime response stream (the upstream connection closed).
266    pub async fn close(&self) -> Result<(), CopilotRequestError> {
267        self.exchange.end_response().await
268    }
269
270    async fn fail(
271        &self,
272        message: impl Into<String>,
273        code: Option<String>,
274    ) -> Result<(), CopilotRequestError> {
275        self.exchange.error_response(message, code).await
276    }
277}
278
279/// A per-connection WebSocket handler. The default implementation
280/// ([`CopilotWebSocketForwarder`]) bridges to the real upstream;
281/// override [`CopilotRequestHandler::open_websocket`] to supply a custom one.
282#[async_trait]
283pub trait CopilotWebSocketHandler: Send + Sync {
284    /// Forward one runtime→upstream message.
285    async fn send_request_message(
286        &self,
287        message: CopilotWebSocketMessage,
288    ) -> Result<(), CopilotRequestError>;
289
290    /// Tear down the upstream connection.
291    async fn close(&self) -> Result<(), CopilotRequestError>;
292}
293
294/// The connection-level Copilot request seam.
295///
296/// One implementor services both transports. Defaults forward transparently to
297/// the real upstream, so overriding nothing yields a pass-through; override a
298/// method to mutate or replace traffic.
299#[async_trait]
300pub trait CopilotRequestHandler: Send + Sync + 'static {
301    /// Service one intercepted HTTP request. Default: forward to the real
302    /// upstream via [`forward_http`]. Override to mutate the request before
303    /// forwarding, mutate the response after, or replace the call entirely.
304    async fn send_request(
305        &self,
306        request: CopilotHttpRequest,
307        _ctx: &CopilotRequestContext,
308    ) -> Result<CopilotHttpResponse, CopilotRequestError> {
309        forward_http(request).await
310    }
311
312    /// Open a per-connection WebSocket handler. Default: a
313    /// [`CopilotWebSocketForwarder`] wired to the real upstream.
314    /// Override to mutate the handshake (URL / headers via `ctx`) or return a
315    /// custom handler.
316    ///
317    /// Unlike the other SDKs, Rust passes `response` — the runtime-facing sink
318    /// for upstream→runtime messages — as a second argument here rather than
319    /// exposing a base-class `send_response_message` helper. A custom handler
320    /// must store this `CopilotWebSocketResponse` in the returned handler struct
321    /// and call [`CopilotWebSocketResponse::send_message`] on it to push
322    /// upstream messages back to the runtime.
323    async fn open_websocket(
324        &self,
325        ctx: &CopilotRequestContext,
326        response: CopilotWebSocketResponse,
327    ) -> Result<Box<dyn CopilotWebSocketHandler>, CopilotRequestError> {
328        let handler = CopilotWebSocketForwarder::builder(ctx.url.clone(), ctx.headers.clone())
329            .connect(response)
330            .await?;
331        Ok(Box::new(handler))
332    }
333}
334
335/// Forward through a shared handler, so an `Arc<H>` can be registered while the
336/// consumer retains a handle (for example to read state the handler records).
337#[async_trait]
338impl<H: CopilotRequestHandler> CopilotRequestHandler for Arc<H> {
339    async fn send_request(
340        &self,
341        request: CopilotHttpRequest,
342        ctx: &CopilotRequestContext,
343    ) -> Result<CopilotHttpResponse, CopilotRequestError> {
344        (**self).send_request(request, ctx).await
345    }
346
347    async fn open_websocket(
348        &self,
349        ctx: &CopilotRequestContext,
350        response: CopilotWebSocketResponse,
351    ) -> Result<Box<dyn CopilotWebSocketHandler>, CopilotRequestError> {
352        (**self).open_websocket(ctx, response).await
353    }
354}
355/// fresh upstream connection.
356const FORBIDDEN_HEADERS: &[&str] = &[
357    "host",
358    "connection",
359    "content-length",
360    "transfer-encoding",
361    "keep-alive",
362    "upgrade",
363    "proxy-connection",
364    "te",
365    "trailer",
366];
367
368fn is_forbidden_header(name: &HeaderName) -> bool {
369    let name = name.as_str();
370    FORBIDDEN_HEADERS.contains(&name) || name.starts_with("sec-websocket")
371}
372
373/// Drop headers that belong to the inbound connection rather than the request.
374fn strip_forbidden_headers(headers: &mut HeaderMap) {
375    let forbidden: Vec<HeaderName> = headers
376        .keys()
377        .filter(|name| is_forbidden_header(name))
378        .cloned()
379        .collect();
380    for name in forbidden {
381        headers.remove(&name);
382    }
383}
384
385static SHARED_HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
386    reqwest::Client::builder()
387        .redirect(reqwest::redirect::Policy::none())
388        .build()
389        .expect("default reqwest client must build")
390});
391
392/// Forward an HTTP request to its real upstream and stream the response back.
393///
394/// This is the default behaviour of [`CopilotRequestHandler::send_request`];
395/// consumers that mutate a request can call it to forward the mutated request.
396pub async fn forward_http(
397    request: CopilotHttpRequest,
398) -> Result<CopilotHttpResponse, CopilotRequestError> {
399    let method = reqwest::Method::from_bytes(request.method.as_bytes())
400        .map_err(|e| CopilotRequestError::InvalidState(format!("invalid HTTP method: {e}")))?;
401
402    let mut headers = request.headers;
403    strip_forbidden_headers(&mut headers);
404
405    let mut builder = SHARED_HTTP_CLIENT
406        .request(method, &request.url)
407        .headers(headers);
408    if !request.body.is_empty() {
409        builder = builder.body(request.body);
410    }
411
412    let response = tokio::select! {
413        _ = request.cancel.cancelled() => {
414            return Err(CopilotRequestError::message("Request cancelled by runtime"));
415        }
416        result = builder.send() => result.map_err(|e| CopilotRequestError::Upstream(e.to_string()))?,
417    };
418
419    let status = response.status().as_u16();
420    let status_text = response.status().canonical_reason().map(str::to_string);
421    let headers = response.headers().clone();
422    let body = response
423        .bytes_stream()
424        .map(|item| item.map_err(|e| CopilotRequestError::Upstream(e.to_string())));
425
426    Ok(CopilotHttpResponse {
427        status,
428        status_text,
429        headers,
430        body: Box::pin(body),
431    })
432}
433
434type UpstreamWrite =
435    futures_util::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
436
437/// Transform applied to a WebSocket message; return `None` to drop it.
438pub type WebSocketTransform =
439    Arc<dyn Fn(CopilotWebSocketMessage) -> Option<CopilotWebSocketMessage> + Send + Sync>;
440
441/// Builder for a [`CopilotWebSocketForwarder`].
442pub struct CopilotWebSocketForwarderBuilder {
443    url: String,
444    headers: HeaderMap,
445    on_send_request_message: Option<WebSocketTransform>,
446    on_send_response_message: Option<WebSocketTransform>,
447}
448
449impl CopilotWebSocketForwarderBuilder {
450    /// Hook runtime→upstream messages (mutate or drop before forwarding).
451    pub fn on_send_request_message(mut self, transform: WebSocketTransform) -> Self {
452        self.on_send_request_message = Some(transform);
453        self
454    }
455
456    /// Hook upstream→runtime messages (mutate or drop before forwarding).
457    pub fn on_send_response_message(mut self, transform: WebSocketTransform) -> Self {
458        self.on_send_response_message = Some(transform);
459        self
460    }
461
462    /// Dial the upstream WebSocket and begin pumping upstream→runtime messages
463    /// into `response`.
464    pub async fn connect(
465        self,
466        response: CopilotWebSocketResponse,
467    ) -> Result<CopilotWebSocketForwarder, CopilotRequestError> {
468        let mut request =
469            self.url.as_str().into_client_request().map_err(|e| {
470                CopilotRequestError::Upstream(format!("invalid websocket url: {e}"))
471            })?;
472        for (name, value) in &self.headers {
473            if is_forbidden_header(name) {
474                continue;
475            }
476            request.headers_mut().append(name.clone(), value.clone());
477        }
478
479        let (stream, _) = connect_async(request)
480            .await
481            .map_err(|e| CopilotRequestError::Upstream(format!("websocket connect failed: {e}")))?;
482        let (write, mut read) = stream.split();
483
484        let cancel = CancellationToken::new();
485        let loop_cancel = cancel.clone();
486        let on_response = self.on_send_response_message.clone();
487        tokio::spawn(async move {
488            loop {
489                tokio::select! {
490                    _ = loop_cancel.cancelled() => break,
491                    msg = read.next() => match msg {
492                        Some(Ok(Message::Text(text))) => {
493                            let message = CopilotWebSocketMessage::from_text(text);
494                            if let Some(out) = apply_transform(&on_response, message) {
495                                let _ = response.send_message(out).await;
496                            }
497                        }
498                        Some(Ok(Message::Binary(data))) => {
499                            let message = CopilotWebSocketMessage { data, binary: true };
500                            if let Some(out) = apply_transform(&on_response, message) {
501                                let _ = response.send_message(out).await;
502                            }
503                        }
504                        Some(Ok(Message::Close(_))) | None => break,
505                        Some(Ok(_)) => continue,
506                        Some(Err(e)) => {
507                            let _ = response.fail(e.to_string(), None).await;
508                            return;
509                        }
510                    }
511                }
512            }
513            let _ = response.close().await;
514        });
515
516        Ok(CopilotWebSocketForwarder {
517            write: AsyncMutex::new(Some(write)),
518            on_send_request_message: self.on_send_request_message,
519            cancel,
520        })
521    }
522}
523
524/// The default WebSocket handler: forwards each runtime message to the real
525/// upstream and each upstream message back to the runtime. Mutate by supplying
526/// transforms on the [builder](CopilotWebSocketForwarder::builder).
527pub struct CopilotWebSocketForwarder {
528    write: AsyncMutex<Option<UpstreamWrite>>,
529    on_send_request_message: Option<WebSocketTransform>,
530    cancel: CancellationToken,
531}
532
533impl CopilotWebSocketForwarder {
534    /// Start building a forwarding handler for `url` with the given upstream
535    /// handshake headers.
536    pub fn builder(url: String, headers: HeaderMap) -> CopilotWebSocketForwarderBuilder {
537        CopilotWebSocketForwarderBuilder {
538            url,
539            headers,
540            on_send_request_message: None,
541            on_send_response_message: None,
542        }
543    }
544}
545
546#[async_trait]
547impl CopilotWebSocketHandler for CopilotWebSocketForwarder {
548    async fn send_request_message(
549        &self,
550        message: CopilotWebSocketMessage,
551    ) -> Result<(), CopilotRequestError> {
552        let Some(message) = apply_transform(&self.on_send_request_message, message) else {
553            return Ok(());
554        };
555        let ws_message = if message.binary {
556            Message::Binary(message.data)
557        } else {
558            let text = match String::from_utf8(message.data) {
559                Ok(text) => text,
560                Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
561            };
562            Message::Text(text)
563        };
564        let mut guard = self.write.lock().await;
565        if let Some(write) = guard.as_mut() {
566            write
567                .send(ws_message)
568                .await
569                .map_err(|e| CopilotRequestError::Upstream(e.to_string()))?;
570        }
571        Ok(())
572    }
573
574    async fn close(&self) -> Result<(), CopilotRequestError> {
575        self.cancel.cancel();
576        let mut guard = self.write.lock().await;
577        if let Some(mut write) = guard.take() {
578            let _ = write.send(Message::Close(None)).await;
579            let _ = write.close().await;
580        }
581        Ok(())
582    }
583}
584
585fn apply_transform(
586    transform: &Option<WebSocketTransform>,
587    message: CopilotWebSocketMessage,
588) -> Option<CopilotWebSocketMessage> {
589    match transform {
590        Some(f) => f(message),
591        None => Some(message),
592    }
593}
594
595/// Mutable response state machine for a single exchange.
596#[derive(Default)]
597struct ResponseState {
598    started: bool,
599    finished: bool,
600}
601
602/// One intercepted request in flight.
603///
604/// Carries the request metadata plus the body byte stream the runtime feeds in
605/// via `httpRequestChunk` frames, and emits the handler's response straight back
606/// to the runtime through the generated `llmInference` server API — a single
607/// object the dispatcher owns and the handler drives.
608/// Request context populated when the matching `httpRequestStart` frame
609/// arrives. Held behind a `OnceLock` so the owning [`CopilotRequestExchange`]
610/// can be created bare by a body chunk that races ahead of its start frame.
611#[derive(Default)]
612struct RequestMeta {
613    session_id: Option<String>,
614    agent_id: Option<String>,
615    parent_agent_id: Option<String>,
616    interaction_type: Option<String>,
617    method: String,
618    url: String,
619    headers: HeaderMap,
620    transport: CopilotRequestTransport,
621}
622
623struct CopilotRequestExchange {
624    request_id: String,
625    meta: OnceLock<RequestMeta>,
626    cancel: CancellationToken,
627    client: Weak<ClientInner>,
628    /// Sender feeding the request body stream. Dropped (set to `None`) on `end`
629    /// or `cancel` to close the stream.
630    body_tx: Mutex<Option<mpsc::UnboundedSender<Vec<u8>>>>,
631    body_rx: AsyncMutex<mpsc::UnboundedReceiver<Vec<u8>>>,
632    state: Mutex<ResponseState>,
633}
634
635impl CopilotRequestExchange {
636    fn new(request_id: String, client: Weak<ClientInner>) -> Self {
637        let (body_tx, body_rx) = mpsc::unbounded_channel();
638        Self {
639            request_id,
640            meta: OnceLock::new(),
641            cancel: CancellationToken::new(),
642            client,
643            body_tx: Mutex::new(Some(body_tx)),
644            body_rx: AsyncMutex::new(body_rx),
645            state: Mutex::new(ResponseState::default()),
646        }
647    }
648
649    /// Fill in the request context once the matching start frame arrives.
650    fn set_context(&self, params: LlmInferenceHttpRequestStartRequest) {
651        let _ = self.meta.set(RequestMeta {
652            session_id: params.session_id.map(SessionId::into_inner),
653            agent_id: params.agent_id,
654            parent_agent_id: params.parent_agent_id,
655            interaction_type: params.interaction_type,
656            method: params.method,
657            url: params.url,
658            headers: headers_from_wire(&params.headers),
659            transport: CopilotRequestTransport::from_wire(params.transport),
660        });
661    }
662
663    /// Request metadata. Always populated before the handler runs; the
664    /// defaulted fallback only guards the (contract-impossible) case of a body
665    /// chunk with no preceding start frame.
666    fn meta(&self) -> &RequestMeta {
667        self.meta.get_or_init(RequestMeta::default)
668    }
669
670    fn context(&self) -> CopilotRequestContext {
671        let meta = self.meta();
672        CopilotRequestContext {
673            request_id: self.request_id.clone(),
674            session_id: meta.session_id.clone(),
675            agent_id: meta.agent_id.clone(),
676            parent_agent_id: meta.parent_agent_id.clone(),
677            interaction_type: meta.interaction_type.clone(),
678            transport: meta.transport,
679            url: meta.url.clone(),
680            headers: meta.headers.clone(),
681            cancel: self.cancel.clone(),
682        }
683    }
684
685    fn client(&self) -> Result<Client, CopilotRequestError> {
686        self.client
687            .upgrade()
688            .map(Client::from_inner)
689            .ok_or(CopilotRequestError::ConnectionClosed)
690    }
691
692    fn request_id(&self) -> RequestId {
693        RequestId::new(self.request_id.clone())
694    }
695
696    // --- Request body feed (driven by the dispatcher as frames arrive) ---
697
698    fn push_chunk(&self, data: Vec<u8>) {
699        if let Some(tx) = self.body_tx.lock().as_ref() {
700            let _ = tx.send(data);
701        }
702    }
703
704    fn push_end(&self) {
705        *self.body_tx.lock() = None;
706    }
707
708    fn push_cancel(&self) {
709        self.cancel.cancel();
710        *self.body_tx.lock() = None;
711    }
712
713    async fn recv_body(&self) -> Option<Vec<u8>> {
714        self.body_rx.lock().await.recv().await
715    }
716
717    async fn drain_body(&self) -> Vec<u8> {
718        let mut buf = Vec::new();
719        let mut rx = self.body_rx.lock().await;
720        while let Some(frame) = rx.recv().await {
721            buf.extend_from_slice(&frame);
722        }
723        buf
724    }
725
726    // --- Response emit (driven by the handler). Strict state machine: ---
727    // start_response once -> 0..N write -> exactly one of
728    // end_response / error_response.
729
730    fn started(&self) -> bool {
731        self.state.lock().started
732    }
733
734    fn finished(&self) -> bool {
735        self.state.lock().finished
736    }
737
738    async fn start_response(
739        &self,
740        status: u16,
741        status_text: Option<String>,
742        headers: HeaderMap,
743    ) -> Result<(), CopilotRequestError> {
744        {
745            let mut state = self.state.lock();
746            if state.started {
747                return Err(CopilotRequestError::InvalidState(
748                    "response start() called twice".to_string(),
749                ));
750            }
751            if state.finished {
752                return Err(CopilotRequestError::InvalidState(
753                    "response already finished".to_string(),
754                ));
755            }
756            state.started = true;
757        }
758        let request = LlmInferenceHttpResponseStartRequest {
759            headers: headers_to_wire(&headers),
760            request_id: self.request_id(),
761            status: i64::from(status),
762            status_text,
763        };
764        self.client()?
765            .rpc()
766            .llm_inference()
767            .http_response_start(request)
768            .await?;
769        Ok(())
770    }
771
772    /// Start the WebSocket upgrade head (status 101) once, ignoring repeat
773    /// calls. The dispatcher emits it eagerly before pumping; later writes call
774    /// this as a harmless no-op backstop.
775    async fn ensure_ws_started(&self) -> Result<(), CopilotRequestError> {
776        if self.started() {
777            return Ok(());
778        }
779        self.start_response(101, None, HeaderMap::new()).await
780    }
781
782    async fn write_text(&self, text: &str) -> Result<(), CopilotRequestError> {
783        self.write(text.to_string(), false).await
784    }
785
786    async fn write_binary(&self, data: &[u8]) -> Result<(), CopilotRequestError> {
787        let encoded = base64::engine::general_purpose::STANDARD.encode(data);
788        self.write(encoded, true).await
789    }
790
791    async fn write(&self, data: String, binary: bool) -> Result<(), CopilotRequestError> {
792        {
793            let state = self.state.lock();
794            if !state.started {
795                return Err(CopilotRequestError::InvalidState(
796                    "response write called before start()".to_string(),
797                ));
798            }
799            if state.finished {
800                return Err(CopilotRequestError::InvalidState(
801                    "response write called after end()/error()".to_string(),
802                ));
803            }
804        }
805        let request = LlmInferenceHttpResponseChunkRequest {
806            binary: binary.then_some(true),
807            data,
808            end: Some(false),
809            error: None,
810            request_id: self.request_id(),
811        };
812        self.client()?
813            .rpc()
814            .llm_inference()
815            .http_response_chunk(request)
816            .await?;
817        Ok(())
818    }
819
820    async fn end_response(&self) -> Result<(), CopilotRequestError> {
821        {
822            let mut state = self.state.lock();
823            if state.finished {
824                return Ok(());
825            }
826            state.finished = true;
827        }
828        let request = LlmInferenceHttpResponseChunkRequest {
829            binary: None,
830            data: String::new(),
831            end: Some(true),
832            error: None,
833            request_id: self.request_id(),
834        };
835        self.client()?
836            .rpc()
837            .llm_inference()
838            .http_response_chunk(request)
839            .await?;
840        Ok(())
841    }
842
843    async fn error_response(
844        &self,
845        message: impl Into<String>,
846        code: Option<String>,
847    ) -> Result<(), CopilotRequestError> {
848        {
849            let mut state = self.state.lock();
850            if state.finished {
851                return Ok(());
852            }
853            state.finished = true;
854        }
855        let request = LlmInferenceHttpResponseChunkRequest {
856            binary: None,
857            data: String::new(),
858            end: Some(true),
859            error: Some(LlmInferenceHttpResponseChunkError {
860                code,
861                message: message.into(),
862            }),
863            request_id: self.request_id(),
864        };
865        self.client()?
866            .rpc()
867            .llm_inference()
868            .http_response_chunk(request)
869            .await?;
870        Ok(())
871    }
872}
873
874/// Drive one exchange through the registered handler, dispatching by transport.
875async fn drive_exchange(
876    exchange: &Arc<CopilotRequestExchange>,
877    handler: &Arc<dyn CopilotRequestHandler>,
878) -> Result<(), CopilotRequestError> {
879    let ctx = exchange.context();
880    let meta = exchange.meta();
881    match meta.transport {
882        CopilotRequestTransport::Http => {
883            let body = exchange.drain_body().await;
884            let request = CopilotHttpRequest {
885                method: meta.method.clone(),
886                url: meta.url.clone(),
887                headers: meta.headers.clone(),
888                body,
889                cancel: ctx.cancel.clone(),
890            };
891            let response = handler.send_request(request, &ctx).await?;
892            stream_http_response(response, exchange, &ctx.cancel).await
893        }
894        CopilotRequestTransport::WebSocket => {
895            // The runtime blocks the WebSocket connect until it receives the 101
896            // response head (the upgrade acknowledgement) and only then forwards
897            // inbound messages as request-body chunks. Emit it eagerly here —
898            // waiting for the first upstream message would deadlock, since the
899            // upstream stays silent until it receives a request message the
900            // runtime won't send before the upgrade completes.
901            exchange.ensure_ws_started().await?;
902            let response = CopilotWebSocketResponse::new(exchange.clone());
903            let ws = handler.open_websocket(&ctx, response).await?;
904            let result = pump_websocket_requests(ws.as_ref(), exchange, &ctx.cancel).await;
905            let _ = ws.close().await;
906            match result {
907                Ok(()) => exchange.end_response().await,
908                Err(err) if ctx.cancel.is_cancelled() => {
909                    exchange
910                        .error_response(
911                            "Request cancelled by runtime",
912                            Some("cancelled".to_string()),
913                        )
914                        .await?;
915                    let _ = err;
916                    Ok(())
917                }
918                Err(err) => Err(err),
919            }
920        }
921    }
922}
923
924/// Stream an HTTP response into the runtime, honouring cancellation.
925async fn stream_http_response(
926    response: CopilotHttpResponse,
927    exchange: &CopilotRequestExchange,
928    cancel: &CancellationToken,
929) -> Result<(), CopilotRequestError> {
930    tokio::select! {
931        biased;
932        // The RPC enqueues its complete frame before its first suspension.
933        // Poll it first even if already cancelled: the writer actor then commits
934        // the head before the terminal error, without waiting for the head ACK.
935        result = exchange.start_response(response.status, response.status_text, response.headers) => {
936            result?;
937        }
938        _ = cancel.cancelled() => {
939            drop(response.body);
940            return exchange
941                .error_response("Request cancelled by runtime", Some("cancelled".to_string()))
942                .await;
943        }
944    }
945
946    let forward = async {
947        let mut reader = HttpResponseReader::new(response.body);
948        let mut chunk = Vec::new();
949        loop {
950            match reader.next_chunk(&mut chunk).await {
951                Ok(true) => {}
952                Ok(false) => return exchange.end_response().await,
953                Err(error) => return exchange.error_response(error.to_string(), None).await,
954            }
955
956            // Keep the same acknowledged write alive while polling the upstream.
957            // Prefer its completion so a ready source cannot delay the next write.
958            let write = exchange.write_binary(&chunk);
959            tokio::pin!(write);
960            loop {
961                tokio::select! {
962                    biased;
963                    result = &mut write => {
964                        result?;
965                        break;
966                    }
967                    () = reader.read_more(), if reader.can_read() => {}
968                }
969            }
970        }
971    };
972    tokio::select! {
973        biased;
974        _ = cancel.cancelled() => {
975            exchange
976                .error_response("Request cancelled by runtime", Some("cancelled".to_string()))
977                .await
978        }
979        result = forward => result,
980    }
981}
982
983/// Forward runtime→upstream WebSocket messages until the runtime closes its side
984/// or cancels.
985async fn pump_websocket_requests(
986    handler: &dyn CopilotWebSocketHandler,
987    exchange: &CopilotRequestExchange,
988    cancel: &CancellationToken,
989) -> Result<(), CopilotRequestError> {
990    loop {
991        tokio::select! {
992            _ = cancel.cancelled() => {
993                return Err(CopilotRequestError::message("Request cancelled by runtime"));
994            }
995            frame = exchange.recv_body() => match frame {
996                Some(data) => {
997                    handler
998                        .send_request_message(CopilotWebSocketMessage { data, binary: false })
999                        .await?;
1000                }
1001                None => return Ok(()),
1002            }
1003        }
1004    }
1005}
1006
1007/// Drive the exchange's response to a terminal state once the handler returns,
1008/// covering handlers that error, get cancelled, or forget to finalize.
1009async fn finalize_exchange(
1010    exchange: &CopilotRequestExchange,
1011    result: Result<(), CopilotRequestError>,
1012) {
1013    match result {
1014        Ok(()) => {
1015            if !exchange.finished() {
1016                fail_via_response(
1017                    exchange,
1018                    502,
1019                    "Copilot request handler returned without finalising the response".to_string(),
1020                )
1021                .await;
1022            }
1023        }
1024        Err(err) => {
1025            if exchange.finished() {
1026                return;
1027            }
1028            if exchange.cancel.is_cancelled() {
1029                if !exchange.started() {
1030                    let _ = exchange.start_response(499, None, HeaderMap::new()).await;
1031                }
1032                let _ = exchange
1033                    .error_response(
1034                        "Request cancelled by runtime",
1035                        Some("cancelled".to_string()),
1036                    )
1037                    .await;
1038            } else {
1039                fail_via_response(exchange, 502, err.to_string()).await;
1040            }
1041        }
1042    }
1043}
1044
1045async fn fail_via_response(exchange: &CopilotRequestExchange, status: u16, message: String) {
1046    if !exchange.started() {
1047        let _ = exchange
1048            .start_response(status, None, HeaderMap::new())
1049            .await;
1050    }
1051    let _ = exchange.error_response(message, None).await;
1052}
1053
1054/// Routes inbound `llmInference.*` requests to the registered handler,
1055/// reassembling each request's streaming body and acking every frame.
1056pub(crate) struct CopilotRequestDispatcher {
1057    handler: Arc<dyn CopilotRequestHandler>,
1058    client: OnceLock<Weak<ClientInner>>,
1059    pending: Mutex<HashMap<String, Arc<CopilotRequestExchange>>>,
1060}
1061
1062impl CopilotRequestDispatcher {
1063    pub(crate) fn new(handler: Arc<dyn CopilotRequestHandler>) -> Self {
1064        Self {
1065            handler,
1066            client: OnceLock::new(),
1067            pending: Mutex::new(HashMap::new()),
1068        }
1069    }
1070
1071    pub(crate) fn set_client(&self, client: Weak<ClientInner>) {
1072        let _ = self.client.set(client);
1073    }
1074
1075    fn client(&self) -> Option<Client> {
1076        self.client
1077            .get()
1078            .and_then(Weak::upgrade)
1079            .map(Client::from_inner)
1080    }
1081
1082    fn client_weak(&self) -> Weak<ClientInner> {
1083        self.client.get().cloned().unwrap_or_else(Weak::new)
1084    }
1085
1086    pub(crate) async fn dispatch(self: &Arc<Self>, request: JsonRpcRequest) {
1087        match request.method.as_str() {
1088            METHOD_HTTP_REQUEST_START => self.handle_start(request).await,
1089            METHOD_HTTP_REQUEST_CHUNK => self.handle_chunk(request).await,
1090            other => {
1091                warn!(method = other, "unknown llmInference request method");
1092                self.send_error(request.id, "unknown llmInference method")
1093                    .await;
1094            }
1095        }
1096    }
1097
1098    fn get_or_create_exchange(&self, request_id: String) -> Arc<CopilotRequestExchange> {
1099        // The runtime dispatches httpRequestStart and httpRequestChunk frames
1100        // independently. get-or-create keeps the adapter correct regardless of
1101        // arrival order: a body chunk (including the terminal end frame) that
1102        // races ahead of its start frame is buffered into the same exchange
1103        // rather than dropped, which would otherwise hang the body drain.
1104        self.pending
1105            .lock()
1106            .entry(request_id.clone())
1107            .or_insert_with(|| {
1108                Arc::new(CopilotRequestExchange::new(request_id, self.client_weak()))
1109            })
1110            .clone()
1111    }
1112
1113    async fn handle_start(self: &Arc<Self>, request: JsonRpcRequest) {
1114        let id = request.id;
1115        let Some(params) = parse_params::<LlmInferenceHttpRequestStartRequest>(&request) else {
1116            self.send_error(id, "invalid llmInference.httpRequestStart params")
1117                .await;
1118            return;
1119        };
1120
1121        // Adopt any exchange a racing chunk already created — with its buffered
1122        // body — rather than dropping those frames.
1123        let request_id = params.request_id.clone().into_inner();
1124        let exchange = self.get_or_create_exchange(request_id.clone());
1125        exchange.set_context(params);
1126
1127        let handler = self.handler.clone();
1128        let dispatcher = Arc::clone(self);
1129        let exchange_for_task = exchange.clone();
1130        tokio::spawn(async move {
1131            let result = drive_exchange(&exchange_for_task, &handler).await;
1132            finalize_exchange(&exchange_for_task, result).await;
1133            dispatcher.remove_pending(&request_id);
1134        });
1135
1136        self.ack(id).await;
1137    }
1138
1139    async fn handle_chunk(&self, request: JsonRpcRequest) {
1140        let id = request.id;
1141        let Some(params) = parse_params::<LlmInferenceHttpRequestChunkRequest>(&request) else {
1142            self.send_error(id, "invalid llmInference.httpRequestChunk params")
1143                .await;
1144            return;
1145        };
1146
1147        // May arrive before the matching start frame; get-or-create so the body
1148        // is buffered, never lost.
1149        let exchange = self.get_or_create_exchange(params.request_id.to_string());
1150        apply_chunk(&exchange, &params);
1151
1152        self.ack(id).await;
1153    }
1154
1155    fn remove_pending(&self, request_id: &str) {
1156        self.pending.lock().remove(request_id);
1157    }
1158
1159    async fn ack(&self, id: u64) {
1160        let Some(client) = self.client() else {
1161            return;
1162        };
1163        let _ = client
1164            .send_response(&JsonRpcResponse {
1165                jsonrpc: "2.0".to_string(),
1166                id,
1167                result: Some(serde_json::json!({})),
1168                error: None,
1169            })
1170            .await;
1171    }
1172
1173    async fn send_error(&self, id: u64, message: &str) {
1174        let Some(client) = self.client() else {
1175            return;
1176        };
1177        let _ = client
1178            .send_response(&JsonRpcResponse {
1179                jsonrpc: "2.0".to_string(),
1180                id,
1181                result: None,
1182                error: Some(crate::JsonRpcError {
1183                    code: error_codes::INTERNAL_ERROR,
1184                    message: message.to_string(),
1185                    data: None,
1186                }),
1187            })
1188            .await;
1189    }
1190}
1191
1192/// Apply one body chunk to a pending request: route data into the body stream,
1193/// or terminate it on `end` / `cancel`.
1194fn apply_chunk(exchange: &CopilotRequestExchange, params: &LlmInferenceHttpRequestChunkRequest) {
1195    if params.cancel == Some(true) {
1196        exchange.push_cancel();
1197        return;
1198    }
1199
1200    if !params.data.is_empty() {
1201        let decoded = if params.binary == Some(true) {
1202            match base64::engine::general_purpose::STANDARD.decode(params.data.as_bytes()) {
1203                Ok(bytes) => bytes,
1204                Err(e) => {
1205                    warn!(error = %e, "failed to decode base64 llmInference body chunk");
1206                    return;
1207                }
1208            }
1209        } else {
1210            params.data.clone().into_bytes()
1211        };
1212        exchange.push_chunk(decoded);
1213    }
1214
1215    if params.end == Some(true) {
1216        exchange.push_end();
1217    }
1218}
1219
1220fn parse_params<T: serde::de::DeserializeOwned>(request: &JsonRpcRequest) -> Option<T> {
1221    request
1222        .params
1223        .as_ref()
1224        .and_then(|p| serde_json::from_value(p.clone()).ok())
1225}
1226
1227/// Convert a wire header map into an [`http::HeaderMap`], skipping any entry the
1228/// `http` crate rejects.
1229fn headers_from_wire(wire: &HashMap<String, Vec<String>>) -> HeaderMap {
1230    let mut headers = HeaderMap::new();
1231    for (name, values) in wire {
1232        let Ok(header_name) = HeaderName::from_bytes(name.as_bytes()) else {
1233            continue;
1234        };
1235        for value in values {
1236            let Ok(header_value) = HeaderValue::from_str(value) else {
1237                continue;
1238            };
1239            headers.append(header_name.clone(), header_value);
1240        }
1241    }
1242    headers
1243}
1244
1245/// Convert an [`http::HeaderMap`] into the wire header map, dropping values that
1246/// are not valid UTF-8.
1247fn headers_to_wire(headers: &HeaderMap) -> HashMap<String, Vec<String>> {
1248    let mut wire: HashMap<String, Vec<String>> = HashMap::new();
1249    for (name, value) in headers {
1250        let Ok(value) = value.to_str() else {
1251            continue;
1252        };
1253        wire.entry(name.as_str().to_string())
1254            .or_default()
1255            .push(value.to_string());
1256    }
1257    wire
1258}