33 *--------------------------------------------------------------------------------------------*/
44
55using GitHub . Copilot . Rpc ;
6+ using System . Buffers ;
67using System . Collections . Concurrent ;
78using System . Diagnostics . CodeAnalysis ;
89using System . Net . WebSockets ;
@@ -76,13 +77,10 @@ public readonly struct CopilotWebSocketMessage(ReadOnlyMemory<byte> data, bool i
7677 public bool IsBinary { get ; } = isBinary ;
7778
7879 /// <summary>Decodes the payload as UTF-8 text.</summary>
79- public string GetText ( ) => Encoding . UTF8 . GetString ( Data . ToArray ( ) ) ;
80+ public string GetText ( ) => Encoding . UTF8 . GetString ( Data . Span ) ;
8081
8182 /// <summary>Creates a text message from a UTF-8 string.</summary>
82- public static CopilotWebSocketMessage Text ( string text ) => new ( Encoding . UTF8 . GetBytes ( text ) , isBinary : false ) ;
83-
84- /// <summary>Creates a binary message from raw bytes.</summary>
85- public static CopilotWebSocketMessage Binary ( ReadOnlyMemory < byte > data ) => new ( data , isBinary : true ) ;
83+ public static CopilotWebSocketMessage FromText ( string text ) => new ( Encoding . UTF8 . GetBytes ( text ) , isBinary : false ) ;
8684}
8785
8886/// <summary>
@@ -253,7 +251,12 @@ internal override async Task OpenAsync()
253251 await socket . ConnectAsync ( ToWebSocketUri ( _url ) , Context . CancellationToken ) . ConfigureAwait ( false ) ;
254252 _upstream = socket ;
255253 _pumpCts = CancellationTokenSource . CreateLinkedTokenSource ( Context . CancellationToken ) ;
256- _responsePump = Task . Run ( ( ) => PumpResponsesAsync ( _pumpCts . Token ) , _pumpCts . Token ) ;
254+
255+ // Start the pump without a cancellation token on Task.Run itself: if the
256+ // linked token is already cancelled, we still want PumpResponsesAsync to
257+ // run so its cleanup (closing the upstream and finalising the response)
258+ // executes rather than the task being cancelled before it ever starts.
259+ _responsePump = Task . Run ( ( ) => PumpResponsesAsync ( _pumpCts . Token ) ) ;
257260 }
258261
259262 /// <summary>
@@ -270,10 +273,10 @@ public override Task SendRequestMessageAsync(CopilotWebSocketMessage message)
270273
271274 var type = message . IsBinary ? WebSocketMessageType . Binary : WebSocketMessageType . Text ;
272275 return _upstream . SendAsync (
273- new ArraySegment < byte > ( message . Data . ToArray ( ) ) ,
276+ message . Data ,
274277 type ,
275278 endOfMessage : true ,
276- Context . CancellationToken ) ;
279+ Context . CancellationToken ) . AsTask ( ) ;
277280 }
278281
279282 /// <inheritdoc />
@@ -346,34 +349,41 @@ await CloseAsync(new CopilotWebSocketCloseStatus
346349
347350 private static async Task < CopilotWebSocketMessage ? > ReceiveMessageAsync ( WebSocket socket , CancellationToken cancellationToken )
348351 {
349- var buffer = new byte [ 16 * 1024 ] ;
350- using var assembled = new MemoryStream ( ) ;
351- WebSocketReceiveResult result ;
352- do
352+ var buffer = ArrayPool < byte > . Shared . Rent ( 16 * 1024 ) ;
353+ try
353354 {
354- try
355- {
356- result = await socket . ReceiveAsync ( new ArraySegment < byte > ( buffer ) , cancellationToken ) . ConfigureAwait ( false ) ;
357- }
358- catch ( OperationCanceledException )
359- {
360- return null ;
361- }
362- catch ( WebSocketException )
355+ using var assembled = new MemoryStream ( ) ;
356+ ValueWebSocketReceiveResult result ;
357+ do
363358 {
364- return null ;
365- }
359+ try
360+ {
361+ result = await socket . ReceiveAsync ( buffer . AsMemory ( ) , cancellationToken ) . ConfigureAwait ( false ) ;
362+ }
363+ catch ( OperationCanceledException )
364+ {
365+ return null ;
366+ }
367+ catch ( WebSocketException )
368+ {
369+ return null ;
370+ }
366371
367- if ( result . MessageType == WebSocketMessageType . Close )
368- {
369- return null ;
372+ if ( result . MessageType == WebSocketMessageType . Close )
373+ {
374+ return null ;
375+ }
376+
377+ assembled . Write ( buffer , 0 , result . Count ) ;
370378 }
379+ while ( ! result . EndOfMessage ) ;
371380
372- assembled . Write ( buffer , 0 , result . Count ) ;
381+ return new CopilotWebSocketMessage ( assembled . ToArray ( ) , result . MessageType == WebSocketMessageType . Binary ) ;
382+ }
383+ finally
384+ {
385+ ArrayPool < byte > . Shared . Return ( buffer ) ;
373386 }
374- while ( ! result . EndOfMessage ) ;
375-
376- return new CopilotWebSocketMessage ( assembled . ToArray ( ) , result . MessageType == WebSocketMessageType . Binary ) ;
377387 }
378388
379389 private static async Task CloseWebSocketQuietlyAsync ( WebSocket socket )
@@ -430,13 +440,34 @@ public class CopilotRequestHandler
430440{
431441 private static readonly HttpClient s_sharedHttpClient = new ( ) ;
432442
443+ private readonly HttpClient _httpClient ;
444+
445+ /// <summary>
446+ /// Initializes a new instance that issues upstream requests using a shared
447+ /// process-wide <see cref="HttpClient"/>.
448+ /// </summary>
449+ public CopilotRequestHandler ( )
450+ : this ( null )
451+ {
452+ }
453+
454+ /// <summary>
455+ /// Initializes a new instance that issues upstream requests using the supplied
456+ /// <see cref="HttpClient"/>, or a shared process-wide instance when <paramref name="httpClient"/> is <see langword="null"/>.
457+ /// </summary>
458+ /// <param name="httpClient">The <see cref="HttpClient"/> to use, or <see langword="null"/> to use the shared instance.</param>
459+ public CopilotRequestHandler ( HttpClient ? httpClient )
460+ {
461+ _httpClient = httpClient ?? s_sharedHttpClient ;
462+ }
463+
433464 /// <summary>
434465 /// Issue the upstream HTTP request. Override to mutate the request before
435466 /// calling <c>base</c>, mutate the returned response after, or replace the
436467 /// call entirely.
437468 /// </summary>
438469 protected virtual Task < HttpResponseMessage > SendRequestAsync ( HttpRequestMessage request , CopilotRequestContext ctx ) =>
439- s_sharedHttpClient . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , ctx . CancellationToken ) ;
470+ _httpClient . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , ctx . CancellationToken ) ;
440471
441472 /// <summary>
442473 /// Open the upstream WebSocket connection. Override to return a custom
@@ -464,7 +495,7 @@ private async Task HandleHttpAsync(LlmInferenceExchange exchange)
464495
465496 private static async Task < HttpRequestMessage > BuildHttpRequestAsync ( LlmInferenceExchange exchange )
466497 {
467- var method = new HttpMethod ( exchange . Method . ToUpperInvariant ( ) ) ;
498+ var method = new HttpMethod ( exchange . Method ) ;
468499 var message = new HttpRequestMessage ( method , exchange . Context . Url ) ;
469500
470501 var hasBody = method != HttpMethod . Get && method != HttpMethod . Head ;
@@ -499,18 +530,10 @@ await exchange.StartResponseAsync(
499530 HeadersToMultiMap ( response ) ) . ConfigureAwait ( false ) ;
500531
501532 var ct = exchange . Context . CancellationToken ;
502- #if NETSTANDARD2_0
503- using var stream = await response . Content . ReadAsStreamAsync ( ) . ConfigureAwait ( false ) ;
504- #else
505533 using var stream = await response . Content . ReadAsStreamAsync ( ct ) . ConfigureAwait ( false ) ;
506- #endif
507534 var buffer = new byte [ 16 * 1024 ] ;
508535 int read ;
509- #if NETSTANDARD2_0
510- while ( ( read = await stream . ReadAsync ( buffer , 0 , buffer . Length , ct ) . ConfigureAwait ( false ) ) > 0 )
511- #else
512536 while ( ( read = await stream . ReadAsync ( buffer . AsMemory ( ) , ct ) . ConfigureAwait ( false ) ) > 0 )
513- #endif
514537 {
515538 await exchange . WriteResponseAsync ( new ReadOnlyMemory < byte > ( buffer , 0 , read ) ) . ConfigureAwait ( false ) ;
516539 }
@@ -579,7 +602,7 @@ private static async Task<byte[]> DrainAsync(IAsyncEnumerable<ReadOnlyMemory<byt
579602 {
580603 if ( chunk . Length > 0 )
581604 {
582- buffer . Write ( chunk . ToArray ( ) , 0 , chunk . Length ) ;
605+ buffer . Write ( chunk . Span ) ;
583606 }
584607 }
585608
0 commit comments