1use 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 crate::generated::api_types::{
44 LlmInferenceHttpRequestChunkRequest, LlmInferenceHttpRequestStartRequest,
45 LlmInferenceHttpRequestStartTransport, LlmInferenceHttpResponseChunkError,
46 LlmInferenceHttpResponseChunkRequest, LlmInferenceHttpResponseStartRequest,
47};
48use crate::{
49 Client, ClientInner, JsonRpcRequest, JsonRpcResponse, RequestId, SessionId, error_codes,
50};
51
52const METHOD_HTTP_REQUEST_START: &str = "llmInference.httpRequestStart";
53const METHOD_HTTP_REQUEST_CHUNK: &str = "llmInference.httpRequestChunk";
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum CopilotRequestTransport {
58 #[default]
60 Http,
61 WebSocket,
64}
65
66impl CopilotRequestTransport {
67 fn from_wire(value: Option<LlmInferenceHttpRequestStartTransport>) -> Self {
68 match value {
69 Some(LlmInferenceHttpRequestStartTransport::Websocket) => Self::WebSocket,
70 _ => Self::Http,
71 }
72 }
73}
74
75#[derive(Debug)]
77#[non_exhaustive]
78pub enum CopilotRequestError {
79 ConnectionClosed,
81
82 InvalidState(String),
85
86 Upstream(String),
88
89 Handler(String),
91
92 Rpc(crate::Error),
94}
95
96impl CopilotRequestError {
97 pub fn message(message: impl Into<String>) -> Self {
100 Self::Handler(message.into())
101 }
102}
103
104impl std::fmt::Display for CopilotRequestError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::ConnectionClosed => {
108 f.write_str("Copilot request response used after RPC connection closed")
109 }
110 Self::InvalidState(message) | Self::Upstream(message) | Self::Handler(message) => {
111 f.write_str(message)
112 }
113 Self::Rpc(err) => write!(f, "{err}"),
114 }
115 }
116}
117
118impl std::error::Error for CopilotRequestError {
119 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
120 match self {
121 Self::Rpc(err) => Some(err),
122 _ => None,
123 }
124 }
125}
126
127impl From<crate::Error> for CopilotRequestError {
128 fn from(err: crate::Error) -> Self {
129 Self::Rpc(err)
130 }
131}
132
133#[derive(Clone)]
136#[non_exhaustive]
137pub struct CopilotRequestContext {
138 pub request_id: String,
140 pub session_id: Option<String>,
143 pub agent_id: Option<String>,
145 pub parent_agent_id: Option<String>,
147 pub interaction_type: Option<String>,
149 pub transport: CopilotRequestTransport,
151 pub url: String,
153 pub headers: HeaderMap,
155 pub cancel: CancellationToken,
157}
158
159pub type CopilotHttpResponseBody =
161 Pin<Box<dyn Stream<Item = Result<Bytes, CopilotRequestError>> + Send>>;
162
163#[non_exhaustive]
165pub struct CopilotHttpRequest {
166 pub method: String,
168 pub url: String,
170 pub headers: HeaderMap,
172 pub body: Vec<u8>,
174 pub cancel: CancellationToken,
176}
177
178#[non_exhaustive]
180pub struct CopilotHttpResponse {
181 pub status: u16,
183 pub status_text: Option<String>,
185 pub headers: HeaderMap,
187 pub body: CopilotHttpResponseBody,
189}
190
191impl CopilotHttpResponse {
192 pub fn new(
194 status: u16,
195 status_text: Option<String>,
196 headers: HeaderMap,
197 body: CopilotHttpResponseBody,
198 ) -> Self {
199 Self {
200 status,
201 status_text,
202 headers,
203 body,
204 }
205 }
206}
207
208#[derive(Clone)]
210pub struct CopilotWebSocketMessage {
211 pub data: Vec<u8>,
213 pub binary: bool,
215}
216
217impl CopilotWebSocketMessage {
218 pub fn from_text(data: impl Into<String>) -> Self {
221 Self {
222 data: data.into().into_bytes(),
223 binary: false,
224 }
225 }
226}
227
228#[derive(Clone)]
231pub struct CopilotWebSocketResponse {
232 exchange: Arc<CopilotRequestExchange>,
233}
234
235impl CopilotWebSocketResponse {
236 fn new(exchange: Arc<CopilotRequestExchange>) -> Self {
237 Self { exchange }
238 }
239
240 pub async fn send_message(
242 &self,
243 message: CopilotWebSocketMessage,
244 ) -> Result<(), CopilotRequestError> {
245 self.exchange.ensure_ws_started().await?;
246 if message.binary {
247 self.exchange.write_binary(&message.data).await
248 } else {
249 let text = String::from_utf8_lossy(&message.data);
250 self.exchange.write_text(&text).await
251 }
252 }
253
254 pub async fn close(&self) -> Result<(), CopilotRequestError> {
256 self.exchange.end_response().await
257 }
258
259 async fn fail(
260 &self,
261 message: impl Into<String>,
262 code: Option<String>,
263 ) -> Result<(), CopilotRequestError> {
264 self.exchange.error_response(message, code).await
265 }
266}
267
268#[async_trait]
272pub trait CopilotWebSocketHandler: Send + Sync {
273 async fn send_request_message(
275 &self,
276 message: CopilotWebSocketMessage,
277 ) -> Result<(), CopilotRequestError>;
278
279 async fn close(&self) -> Result<(), CopilotRequestError>;
281}
282
283#[async_trait]
289pub trait CopilotRequestHandler: Send + Sync + 'static {
290 async fn send_request(
294 &self,
295 request: CopilotHttpRequest,
296 _ctx: &CopilotRequestContext,
297 ) -> Result<CopilotHttpResponse, CopilotRequestError> {
298 forward_http(request).await
299 }
300
301 async fn open_websocket(
313 &self,
314 ctx: &CopilotRequestContext,
315 response: CopilotWebSocketResponse,
316 ) -> Result<Box<dyn CopilotWebSocketHandler>, CopilotRequestError> {
317 let handler = CopilotWebSocketForwarder::builder(ctx.url.clone(), ctx.headers.clone())
318 .connect(response)
319 .await?;
320 Ok(Box::new(handler))
321 }
322}
323
324#[async_trait]
327impl<H: CopilotRequestHandler> CopilotRequestHandler for Arc<H> {
328 async fn send_request(
329 &self,
330 request: CopilotHttpRequest,
331 ctx: &CopilotRequestContext,
332 ) -> Result<CopilotHttpResponse, CopilotRequestError> {
333 (**self).send_request(request, ctx).await
334 }
335
336 async fn open_websocket(
337 &self,
338 ctx: &CopilotRequestContext,
339 response: CopilotWebSocketResponse,
340 ) -> Result<Box<dyn CopilotWebSocketHandler>, CopilotRequestError> {
341 (**self).open_websocket(ctx, response).await
342 }
343}
344const FORBIDDEN_HEADERS: &[&str] = &[
346 "host",
347 "connection",
348 "content-length",
349 "transfer-encoding",
350 "keep-alive",
351 "upgrade",
352 "proxy-connection",
353 "te",
354 "trailer",
355];
356
357fn is_forbidden_header(name: &HeaderName) -> bool {
358 let name = name.as_str();
359 FORBIDDEN_HEADERS.contains(&name) || name.starts_with("sec-websocket")
360}
361
362fn strip_forbidden_headers(headers: &mut HeaderMap) {
364 let forbidden: Vec<HeaderName> = headers
365 .keys()
366 .filter(|name| is_forbidden_header(name))
367 .cloned()
368 .collect();
369 for name in forbidden {
370 headers.remove(&name);
371 }
372}
373
374static SHARED_HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
375 reqwest::Client::builder()
376 .redirect(reqwest::redirect::Policy::none())
377 .build()
378 .expect("default reqwest client must build")
379});
380
381pub async fn forward_http(
386 request: CopilotHttpRequest,
387) -> Result<CopilotHttpResponse, CopilotRequestError> {
388 let method = reqwest::Method::from_bytes(request.method.as_bytes())
389 .map_err(|e| CopilotRequestError::InvalidState(format!("invalid HTTP method: {e}")))?;
390
391 let mut headers = request.headers;
392 strip_forbidden_headers(&mut headers);
393
394 let mut builder = SHARED_HTTP_CLIENT
395 .request(method, &request.url)
396 .headers(headers);
397 if !request.body.is_empty() {
398 builder = builder.body(request.body);
399 }
400
401 let response = tokio::select! {
402 _ = request.cancel.cancelled() => {
403 return Err(CopilotRequestError::message("Request cancelled by runtime"));
404 }
405 result = builder.send() => result.map_err(|e| CopilotRequestError::Upstream(e.to_string()))?,
406 };
407
408 let status = response.status().as_u16();
409 let status_text = response.status().canonical_reason().map(str::to_string);
410 let headers = response.headers().clone();
411 let body = response
412 .bytes_stream()
413 .map(|item| item.map_err(|e| CopilotRequestError::Upstream(e.to_string())));
414
415 Ok(CopilotHttpResponse {
416 status,
417 status_text,
418 headers,
419 body: Box::pin(body),
420 })
421}
422
423type UpstreamWrite =
424 futures_util::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
425
426pub type WebSocketTransform =
428 Arc<dyn Fn(CopilotWebSocketMessage) -> Option<CopilotWebSocketMessage> + Send + Sync>;
429
430pub struct CopilotWebSocketForwarderBuilder {
432 url: String,
433 headers: HeaderMap,
434 on_send_request_message: Option<WebSocketTransform>,
435 on_send_response_message: Option<WebSocketTransform>,
436}
437
438impl CopilotWebSocketForwarderBuilder {
439 pub fn on_send_request_message(mut self, transform: WebSocketTransform) -> Self {
441 self.on_send_request_message = Some(transform);
442 self
443 }
444
445 pub fn on_send_response_message(mut self, transform: WebSocketTransform) -> Self {
447 self.on_send_response_message = Some(transform);
448 self
449 }
450
451 pub async fn connect(
454 self,
455 response: CopilotWebSocketResponse,
456 ) -> Result<CopilotWebSocketForwarder, CopilotRequestError> {
457 let mut request =
458 self.url.as_str().into_client_request().map_err(|e| {
459 CopilotRequestError::Upstream(format!("invalid websocket url: {e}"))
460 })?;
461 for (name, value) in &self.headers {
462 if is_forbidden_header(name) {
463 continue;
464 }
465 request.headers_mut().append(name.clone(), value.clone());
466 }
467
468 let (stream, _) = connect_async(request)
469 .await
470 .map_err(|e| CopilotRequestError::Upstream(format!("websocket connect failed: {e}")))?;
471 let (write, mut read) = stream.split();
472
473 let cancel = CancellationToken::new();
474 let loop_cancel = cancel.clone();
475 let on_response = self.on_send_response_message.clone();
476 tokio::spawn(async move {
477 loop {
478 tokio::select! {
479 _ = loop_cancel.cancelled() => break,
480 msg = read.next() => match msg {
481 Some(Ok(Message::Text(text))) => {
482 let message = CopilotWebSocketMessage::from_text(text);
483 if let Some(out) = apply_transform(&on_response, message) {
484 let _ = response.send_message(out).await;
485 }
486 }
487 Some(Ok(Message::Binary(data))) => {
488 let message = CopilotWebSocketMessage { data, binary: true };
489 if let Some(out) = apply_transform(&on_response, message) {
490 let _ = response.send_message(out).await;
491 }
492 }
493 Some(Ok(Message::Close(_))) | None => break,
494 Some(Ok(_)) => continue,
495 Some(Err(e)) => {
496 let _ = response.fail(e.to_string(), None).await;
497 return;
498 }
499 }
500 }
501 }
502 let _ = response.close().await;
503 });
504
505 Ok(CopilotWebSocketForwarder {
506 write: AsyncMutex::new(Some(write)),
507 on_send_request_message: self.on_send_request_message,
508 cancel,
509 })
510 }
511}
512
513pub struct CopilotWebSocketForwarder {
517 write: AsyncMutex<Option<UpstreamWrite>>,
518 on_send_request_message: Option<WebSocketTransform>,
519 cancel: CancellationToken,
520}
521
522impl CopilotWebSocketForwarder {
523 pub fn builder(url: String, headers: HeaderMap) -> CopilotWebSocketForwarderBuilder {
526 CopilotWebSocketForwarderBuilder {
527 url,
528 headers,
529 on_send_request_message: None,
530 on_send_response_message: None,
531 }
532 }
533}
534
535#[async_trait]
536impl CopilotWebSocketHandler for CopilotWebSocketForwarder {
537 async fn send_request_message(
538 &self,
539 message: CopilotWebSocketMessage,
540 ) -> Result<(), CopilotRequestError> {
541 let Some(message) = apply_transform(&self.on_send_request_message, message) else {
542 return Ok(());
543 };
544 let ws_message = if message.binary {
545 Message::Binary(message.data)
546 } else {
547 let text = match String::from_utf8(message.data) {
548 Ok(text) => text,
549 Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
550 };
551 Message::Text(text)
552 };
553 let mut guard = self.write.lock().await;
554 if let Some(write) = guard.as_mut() {
555 write
556 .send(ws_message)
557 .await
558 .map_err(|e| CopilotRequestError::Upstream(e.to_string()))?;
559 }
560 Ok(())
561 }
562
563 async fn close(&self) -> Result<(), CopilotRequestError> {
564 self.cancel.cancel();
565 let mut guard = self.write.lock().await;
566 if let Some(mut write) = guard.take() {
567 let _ = write.send(Message::Close(None)).await;
568 let _ = write.close().await;
569 }
570 Ok(())
571 }
572}
573
574fn apply_transform(
575 transform: &Option<WebSocketTransform>,
576 message: CopilotWebSocketMessage,
577) -> Option<CopilotWebSocketMessage> {
578 match transform {
579 Some(f) => f(message),
580 None => Some(message),
581 }
582}
583
584#[derive(Default)]
586struct ResponseState {
587 started: bool,
588 finished: bool,
589}
590
591#[derive(Default)]
601struct RequestMeta {
602 session_id: Option<String>,
603 agent_id: Option<String>,
604 parent_agent_id: Option<String>,
605 interaction_type: Option<String>,
606 method: String,
607 url: String,
608 headers: HeaderMap,
609 transport: CopilotRequestTransport,
610}
611
612struct CopilotRequestExchange {
613 request_id: String,
614 meta: OnceLock<RequestMeta>,
615 cancel: CancellationToken,
616 client: Weak<ClientInner>,
617 body_tx: Mutex<Option<mpsc::UnboundedSender<Vec<u8>>>>,
620 body_rx: AsyncMutex<mpsc::UnboundedReceiver<Vec<u8>>>,
621 state: Mutex<ResponseState>,
622}
623
624impl CopilotRequestExchange {
625 fn new(request_id: String, client: Weak<ClientInner>) -> Self {
626 let (body_tx, body_rx) = mpsc::unbounded_channel();
627 Self {
628 request_id,
629 meta: OnceLock::new(),
630 cancel: CancellationToken::new(),
631 client,
632 body_tx: Mutex::new(Some(body_tx)),
633 body_rx: AsyncMutex::new(body_rx),
634 state: Mutex::new(ResponseState::default()),
635 }
636 }
637
638 fn set_context(&self, params: LlmInferenceHttpRequestStartRequest) {
640 let _ = self.meta.set(RequestMeta {
641 session_id: params.session_id.map(SessionId::into_inner),
642 agent_id: params.agent_id,
643 parent_agent_id: params.parent_agent_id,
644 interaction_type: params.interaction_type,
645 method: params.method,
646 url: params.url,
647 headers: headers_from_wire(¶ms.headers),
648 transport: CopilotRequestTransport::from_wire(params.transport),
649 });
650 }
651
652 fn meta(&self) -> &RequestMeta {
656 self.meta.get_or_init(RequestMeta::default)
657 }
658
659 fn context(&self) -> CopilotRequestContext {
660 let meta = self.meta();
661 CopilotRequestContext {
662 request_id: self.request_id.clone(),
663 session_id: meta.session_id.clone(),
664 agent_id: meta.agent_id.clone(),
665 parent_agent_id: meta.parent_agent_id.clone(),
666 interaction_type: meta.interaction_type.clone(),
667 transport: meta.transport,
668 url: meta.url.clone(),
669 headers: meta.headers.clone(),
670 cancel: self.cancel.clone(),
671 }
672 }
673
674 fn client(&self) -> Result<Client, CopilotRequestError> {
675 self.client
676 .upgrade()
677 .map(Client::from_inner)
678 .ok_or(CopilotRequestError::ConnectionClosed)
679 }
680
681 fn request_id(&self) -> RequestId {
682 RequestId::new(self.request_id.clone())
683 }
684
685 fn push_chunk(&self, data: Vec<u8>) {
688 if let Some(tx) = self.body_tx.lock().as_ref() {
689 let _ = tx.send(data);
690 }
691 }
692
693 fn push_end(&self) {
694 *self.body_tx.lock() = None;
695 }
696
697 fn push_cancel(&self) {
698 self.cancel.cancel();
699 *self.body_tx.lock() = None;
700 }
701
702 async fn recv_body(&self) -> Option<Vec<u8>> {
703 self.body_rx.lock().await.recv().await
704 }
705
706 async fn drain_body(&self) -> Vec<u8> {
707 let mut buf = Vec::new();
708 let mut rx = self.body_rx.lock().await;
709 while let Some(frame) = rx.recv().await {
710 buf.extend_from_slice(&frame);
711 }
712 buf
713 }
714
715 fn started(&self) -> bool {
720 self.state.lock().started
721 }
722
723 fn finished(&self) -> bool {
724 self.state.lock().finished
725 }
726
727 async fn start_response(
728 &self,
729 status: u16,
730 status_text: Option<String>,
731 headers: HeaderMap,
732 ) -> Result<(), CopilotRequestError> {
733 {
734 let mut state = self.state.lock();
735 if state.started {
736 return Err(CopilotRequestError::InvalidState(
737 "response start() called twice".to_string(),
738 ));
739 }
740 if state.finished {
741 return Err(CopilotRequestError::InvalidState(
742 "response already finished".to_string(),
743 ));
744 }
745 state.started = true;
746 }
747 let request = LlmInferenceHttpResponseStartRequest {
748 headers: headers_to_wire(&headers),
749 request_id: self.request_id(),
750 status: i64::from(status),
751 status_text,
752 };
753 self.client()?
754 .rpc()
755 .llm_inference()
756 .http_response_start(request)
757 .await?;
758 Ok(())
759 }
760
761 async fn ensure_ws_started(&self) -> Result<(), CopilotRequestError> {
765 if self.started() {
766 return Ok(());
767 }
768 self.start_response(101, None, HeaderMap::new()).await
769 }
770
771 async fn write_text(&self, text: &str) -> Result<(), CopilotRequestError> {
772 self.write(text.to_string(), false).await
773 }
774
775 async fn write_binary(&self, data: &[u8]) -> Result<(), CopilotRequestError> {
776 let encoded = base64::engine::general_purpose::STANDARD.encode(data);
777 self.write(encoded, true).await
778 }
779
780 async fn write(&self, data: String, binary: bool) -> Result<(), CopilotRequestError> {
781 {
782 let state = self.state.lock();
783 if !state.started {
784 return Err(CopilotRequestError::InvalidState(
785 "response write called before start()".to_string(),
786 ));
787 }
788 if state.finished {
789 return Err(CopilotRequestError::InvalidState(
790 "response write called after end()/error()".to_string(),
791 ));
792 }
793 }
794 let request = LlmInferenceHttpResponseChunkRequest {
795 binary: binary.then_some(true),
796 data,
797 end: Some(false),
798 error: None,
799 request_id: self.request_id(),
800 };
801 self.client()?
802 .rpc()
803 .llm_inference()
804 .http_response_chunk(request)
805 .await?;
806 Ok(())
807 }
808
809 async fn end_response(&self) -> Result<(), CopilotRequestError> {
810 {
811 let mut state = self.state.lock();
812 if state.finished {
813 return Ok(());
814 }
815 state.finished = true;
816 }
817 let request = LlmInferenceHttpResponseChunkRequest {
818 binary: None,
819 data: String::new(),
820 end: Some(true),
821 error: None,
822 request_id: self.request_id(),
823 };
824 self.client()?
825 .rpc()
826 .llm_inference()
827 .http_response_chunk(request)
828 .await?;
829 Ok(())
830 }
831
832 async fn error_response(
833 &self,
834 message: impl Into<String>,
835 code: Option<String>,
836 ) -> Result<(), CopilotRequestError> {
837 {
838 let mut state = self.state.lock();
839 if state.finished {
840 return Ok(());
841 }
842 state.finished = true;
843 }
844 let request = LlmInferenceHttpResponseChunkRequest {
845 binary: None,
846 data: String::new(),
847 end: Some(true),
848 error: Some(LlmInferenceHttpResponseChunkError {
849 code,
850 message: message.into(),
851 }),
852 request_id: self.request_id(),
853 };
854 self.client()?
855 .rpc()
856 .llm_inference()
857 .http_response_chunk(request)
858 .await?;
859 Ok(())
860 }
861}
862
863async fn drive_exchange(
865 exchange: &Arc<CopilotRequestExchange>,
866 handler: &Arc<dyn CopilotRequestHandler>,
867) -> Result<(), CopilotRequestError> {
868 let ctx = exchange.context();
869 let meta = exchange.meta();
870 match meta.transport {
871 CopilotRequestTransport::Http => {
872 let body = exchange.drain_body().await;
873 let request = CopilotHttpRequest {
874 method: meta.method.clone(),
875 url: meta.url.clone(),
876 headers: meta.headers.clone(),
877 body,
878 cancel: ctx.cancel.clone(),
879 };
880 let response = handler.send_request(request, &ctx).await?;
881 stream_http_response(response, exchange, &ctx.cancel).await
882 }
883 CopilotRequestTransport::WebSocket => {
884 exchange.ensure_ws_started().await?;
891 let response = CopilotWebSocketResponse::new(exchange.clone());
892 let ws = handler.open_websocket(&ctx, response).await?;
893 let result = pump_websocket_requests(ws.as_ref(), exchange, &ctx.cancel).await;
894 let _ = ws.close().await;
895 match result {
896 Ok(()) => exchange.end_response().await,
897 Err(err) if ctx.cancel.is_cancelled() => {
898 exchange
899 .error_response(
900 "Request cancelled by runtime",
901 Some("cancelled".to_string()),
902 )
903 .await?;
904 let _ = err;
905 Ok(())
906 }
907 Err(err) => Err(err),
908 }
909 }
910 }
911}
912
913async fn stream_http_response(
915 response: CopilotHttpResponse,
916 exchange: &CopilotRequestExchange,
917 cancel: &CancellationToken,
918) -> Result<(), CopilotRequestError> {
919 exchange
920 .start_response(response.status, response.status_text, response.headers)
921 .await?;
922
923 let mut body = response.body;
924 loop {
925 tokio::select! {
926 _ = cancel.cancelled() => {
927 return exchange
928 .error_response("Request cancelled by runtime", Some("cancelled".to_string()))
929 .await;
930 }
931 next = body.next() => match next {
932 Some(Ok(chunk)) => {
933 for piece in chunk.chunks(32 * 1024) {
934 exchange.write_binary(piece).await?;
935 }
936 }
937 Some(Err(e)) => {
938 return exchange.error_response(e.to_string(), None).await;
939 }
940 None => break,
941 }
942 }
943 }
944 exchange.end_response().await
945}
946
947async fn pump_websocket_requests(
950 handler: &dyn CopilotWebSocketHandler,
951 exchange: &CopilotRequestExchange,
952 cancel: &CancellationToken,
953) -> Result<(), CopilotRequestError> {
954 loop {
955 tokio::select! {
956 _ = cancel.cancelled() => {
957 return Err(CopilotRequestError::message("Request cancelled by runtime"));
958 }
959 frame = exchange.recv_body() => match frame {
960 Some(data) => {
961 handler
962 .send_request_message(CopilotWebSocketMessage { data, binary: false })
963 .await?;
964 }
965 None => return Ok(()),
966 }
967 }
968 }
969}
970
971async fn finalize_exchange(
974 exchange: &CopilotRequestExchange,
975 result: Result<(), CopilotRequestError>,
976) {
977 match result {
978 Ok(()) => {
979 if !exchange.finished() {
980 fail_via_response(
981 exchange,
982 502,
983 "Copilot request handler returned without finalising the response".to_string(),
984 )
985 .await;
986 }
987 }
988 Err(err) => {
989 if exchange.finished() {
990 return;
991 }
992 if exchange.cancel.is_cancelled() {
993 if !exchange.started() {
994 let _ = exchange.start_response(499, None, HeaderMap::new()).await;
995 }
996 let _ = exchange
997 .error_response(
998 "Request cancelled by runtime",
999 Some("cancelled".to_string()),
1000 )
1001 .await;
1002 } else {
1003 fail_via_response(exchange, 502, err.to_string()).await;
1004 }
1005 }
1006 }
1007}
1008
1009async fn fail_via_response(exchange: &CopilotRequestExchange, status: u16, message: String) {
1010 if !exchange.started() {
1011 let _ = exchange
1012 .start_response(status, None, HeaderMap::new())
1013 .await;
1014 }
1015 let _ = exchange.error_response(message, None).await;
1016}
1017
1018pub(crate) struct CopilotRequestDispatcher {
1021 handler: Arc<dyn CopilotRequestHandler>,
1022 client: OnceLock<Weak<ClientInner>>,
1023 pending: Mutex<HashMap<String, Arc<CopilotRequestExchange>>>,
1024}
1025
1026impl CopilotRequestDispatcher {
1027 pub(crate) fn new(handler: Arc<dyn CopilotRequestHandler>) -> Self {
1028 Self {
1029 handler,
1030 client: OnceLock::new(),
1031 pending: Mutex::new(HashMap::new()),
1032 }
1033 }
1034
1035 pub(crate) fn set_client(&self, client: Weak<ClientInner>) {
1036 let _ = self.client.set(client);
1037 }
1038
1039 fn client(&self) -> Option<Client> {
1040 self.client
1041 .get()
1042 .and_then(Weak::upgrade)
1043 .map(Client::from_inner)
1044 }
1045
1046 fn client_weak(&self) -> Weak<ClientInner> {
1047 self.client.get().cloned().unwrap_or_else(Weak::new)
1048 }
1049
1050 pub(crate) async fn dispatch(self: &Arc<Self>, request: JsonRpcRequest) {
1051 match request.method.as_str() {
1052 METHOD_HTTP_REQUEST_START => self.handle_start(request).await,
1053 METHOD_HTTP_REQUEST_CHUNK => self.handle_chunk(request).await,
1054 other => {
1055 warn!(method = other, "unknown llmInference request method");
1056 self.send_error(request.id, "unknown llmInference method")
1057 .await;
1058 }
1059 }
1060 }
1061
1062 fn get_or_create_exchange(&self, request_id: String) -> Arc<CopilotRequestExchange> {
1063 self.pending
1069 .lock()
1070 .entry(request_id.clone())
1071 .or_insert_with(|| {
1072 Arc::new(CopilotRequestExchange::new(request_id, self.client_weak()))
1073 })
1074 .clone()
1075 }
1076
1077 async fn handle_start(self: &Arc<Self>, request: JsonRpcRequest) {
1078 let id = request.id;
1079 let Some(params) = parse_params::<LlmInferenceHttpRequestStartRequest>(&request) else {
1080 self.send_error(id, "invalid llmInference.httpRequestStart params")
1081 .await;
1082 return;
1083 };
1084
1085 let request_id = params.request_id.clone().into_inner();
1088 let exchange = self.get_or_create_exchange(request_id.clone());
1089 exchange.set_context(params);
1090
1091 let handler = self.handler.clone();
1092 let dispatcher = Arc::clone(self);
1093 let exchange_for_task = exchange.clone();
1094 tokio::spawn(async move {
1095 let result = drive_exchange(&exchange_for_task, &handler).await;
1096 finalize_exchange(&exchange_for_task, result).await;
1097 dispatcher.remove_pending(&request_id);
1098 });
1099
1100 self.ack(id).await;
1101 }
1102
1103 async fn handle_chunk(&self, request: JsonRpcRequest) {
1104 let id = request.id;
1105 let Some(params) = parse_params::<LlmInferenceHttpRequestChunkRequest>(&request) else {
1106 self.send_error(id, "invalid llmInference.httpRequestChunk params")
1107 .await;
1108 return;
1109 };
1110
1111 let exchange = self.get_or_create_exchange(params.request_id.to_string());
1114 apply_chunk(&exchange, ¶ms);
1115
1116 self.ack(id).await;
1117 }
1118
1119 fn remove_pending(&self, request_id: &str) {
1120 self.pending.lock().remove(request_id);
1121 }
1122
1123 async fn ack(&self, id: u64) {
1124 let Some(client) = self.client() else {
1125 return;
1126 };
1127 let _ = client
1128 .send_response(&JsonRpcResponse {
1129 jsonrpc: "2.0".to_string(),
1130 id,
1131 result: Some(serde_json::json!({})),
1132 error: None,
1133 })
1134 .await;
1135 }
1136
1137 async fn send_error(&self, id: u64, message: &str) {
1138 let Some(client) = self.client() else {
1139 return;
1140 };
1141 let _ = client
1142 .send_response(&JsonRpcResponse {
1143 jsonrpc: "2.0".to_string(),
1144 id,
1145 result: None,
1146 error: Some(crate::JsonRpcError {
1147 code: error_codes::INTERNAL_ERROR,
1148 message: message.to_string(),
1149 data: None,
1150 }),
1151 })
1152 .await;
1153 }
1154}
1155
1156fn apply_chunk(exchange: &CopilotRequestExchange, params: &LlmInferenceHttpRequestChunkRequest) {
1159 if params.cancel == Some(true) {
1160 exchange.push_cancel();
1161 return;
1162 }
1163
1164 if !params.data.is_empty() {
1165 let decoded = if params.binary == Some(true) {
1166 match base64::engine::general_purpose::STANDARD.decode(params.data.as_bytes()) {
1167 Ok(bytes) => bytes,
1168 Err(e) => {
1169 warn!(error = %e, "failed to decode base64 llmInference body chunk");
1170 return;
1171 }
1172 }
1173 } else {
1174 params.data.clone().into_bytes()
1175 };
1176 exchange.push_chunk(decoded);
1177 }
1178
1179 if params.end == Some(true) {
1180 exchange.push_end();
1181 }
1182}
1183
1184fn parse_params<T: serde::de::DeserializeOwned>(request: &JsonRpcRequest) -> Option<T> {
1185 request
1186 .params
1187 .as_ref()
1188 .and_then(|p| serde_json::from_value(p.clone()).ok())
1189}
1190
1191fn headers_from_wire(wire: &HashMap<String, Vec<String>>) -> HeaderMap {
1194 let mut headers = HeaderMap::new();
1195 for (name, values) in wire {
1196 let Ok(header_name) = HeaderName::from_bytes(name.as_bytes()) else {
1197 continue;
1198 };
1199 for value in values {
1200 let Ok(header_value) = HeaderValue::from_str(value) else {
1201 continue;
1202 };
1203 headers.append(header_name.clone(), header_value);
1204 }
1205 }
1206 headers
1207}
1208
1209fn headers_to_wire(headers: &HeaderMap) -> HashMap<String, Vec<String>> {
1212 let mut wire: HashMap<String, Vec<String>> = HashMap::new();
1213 for (name, value) in headers {
1214 let Ok(value) = value.to_str() else {
1215 continue;
1216 };
1217 wire.entry(name.as_str().to_string())
1218 .or_default()
1219 .push(value.to_string());
1220 }
1221 wire
1222}