@@ -85,6 +85,13 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
8585 private List < ModelInfo > ? _modelsCache ;
8686 private ServerRpc ? _serverRpc ;
8787
88+ /// <summary>
89+ /// Client-global RPC handlers (e.g. the LLM inference provider adapter),
90+ /// built once at construction when the corresponding option is configured and
91+ /// registered on every connection. Null when no client-global API is enabled.
92+ /// </summary>
93+ private readonly ClientGlobalApiHandlers ? _clientGlobalApis ;
94+
8895 private sealed record LifecycleSubscription ( Type EventType , Action < SessionLifecycleEvent > Handler ) ;
8996
9097 /// <summary>
@@ -165,6 +172,8 @@ public CopilotClient(CopilotClientOptions? options = null)
165172 _logger = _options . Logger ?? NullLogger . Instance ;
166173 _onListModels = _options . OnListModels ;
167174
175+ _clientGlobalApis = BuildClientGlobalApis ( ) ;
176+
168177 // Empty mode: validate at construction time that the app supplied a
169178 // per-session persistence location. The runtime is mode-agnostic, so
170179 // without this check it would silently fall back to ~/.copilot, which
@@ -276,6 +285,8 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
276285 sessionFsTimestamp ) ;
277286 }
278287
288+ await ConfigureLlmInferenceAsync ( ct ) ;
289+
279290 LoggingHelpers . LogTiming ( _logger , LogLevel . Debug , null ,
280291 "CopilotClient.StartAsync complete. Elapsed={Elapsed}" ,
281292 startTimestamp ) ;
@@ -426,15 +437,13 @@ private async Task CleanupConnectionAsync(List<Exception>? errors, bool graceful
426437
427438 private async Task CleanupConnectionAsync ( Connection ctx , List < Exception > ? errors , bool gracefulRuntimeShutdown )
428439 {
429- var runtimeShutdownCompleted = false ;
430440 if ( gracefulRuntimeShutdown && ctx . CliProcess is not null )
431441 {
432442 var runtimeShutdownTimestamp = Stopwatch . GetTimestamp ( ) ;
433443 try
434444 {
435445 using var cancellation = new CancellationTokenSource ( s_runtimeShutdownTimeout ) ;
436446 await ctx . Server . Runtime . ShutdownAsync ( cancellation . Token ) ;
437- runtimeShutdownCompleted = true ;
438447 LoggingHelpers . LogTiming ( _logger , LogLevel . Debug , null ,
439448 "CopilotClient.StopAsync runtime shutdown complete. Elapsed={Elapsed}" ,
440449 runtimeShutdownTimestamp ) ;
@@ -466,42 +475,24 @@ or IOException
466475
467476 if ( ctx . CliProcess is { } childProcess )
468477 {
469- await CleanupCliProcessAsync ( childProcess , ctx . StderrPump , errors , _logger , runtimeShutdownCompleted ) ;
478+ await CleanupCliProcessAsync ( childProcess , ctx . StderrPump , errors , _logger ) ;
470479 }
471480 }
472481
473- private static async Task CleanupCliProcessAsync ( Process childProcess , ProcessStderrPump ? stderrPump , List < Exception > ? errors , ILogger ? logger , bool waitForGracefulExit = false )
482+ private static async Task CleanupCliProcessAsync ( Process childProcess , ProcessStderrPump ? stderrPump , List < Exception > ? errors , ILogger ? logger )
474483 {
475484 stderrPump ? . Cancel ( ) ;
476485
477486 try
478487 {
479488 if ( ! childProcess . HasExited )
480489 {
481- if ( waitForGracefulExit )
482- {
483- var shutdownWaitTimestamp = Stopwatch . GetTimestamp ( ) ;
484- try
485- {
486- await childProcess . WaitForExitAsync ( ) . WaitAsync ( s_runtimeShutdownTimeout ) ;
487- }
488- catch ( TimeoutException ex )
489- {
490- if ( logger is not null )
491- {
492- LoggingHelpers . LogTiming ( logger , LogLevel . Debug , ex ,
493- "Timed out waiting for runtime process to exit after graceful shutdown. Elapsed={Elapsed}, Timeout={Timeout}" ,
494- shutdownWaitTimestamp ,
495- s_runtimeShutdownTimeout ) ;
496- }
497- }
498- }
499-
500- if ( childProcess . HasExited )
501- {
502- return ;
503- }
504-
490+ // The runtime completes all cleanup before responding to
491+ // runtime.shutdown and then leaves termination to us; it
492+ // deliberately keeps its JSON-RPC server alive to send the
493+ // response and never self-exits. Waiting for a self-exit that
494+ // will never come just wastes time, so terminate the child
495+ // immediately and only wait to reap it.
505496 childProcess . Kill ( entireProcessTree : true ) ;
506497 // Kill is asynchronous; wait for the root CLI process to exit so cleanup callers
507498 // do not observe StopAsync/DisposeAsync completion while it is still tearing down.
@@ -1685,6 +1676,39 @@ await Rpc.SessionFs.SetProviderAsync(
16851676 cancellationToken : cancellationToken ) ;
16861677 }
16871678
1679+ /// <summary>
1680+ /// Builds the client-global RPC handler bag at construction time. Currently
1681+ /// only the LLM inference provider adapter is registered; returns null when no
1682+ /// client-global API is configured so the registration is skipped entirely.
1683+ /// </summary>
1684+ private ClientGlobalApiHandlers ? BuildClientGlobalApis ( )
1685+ {
1686+ var handler = _options . RequestHandler ;
1687+ if ( handler is null )
1688+ {
1689+ return null ;
1690+ }
1691+
1692+ return new ClientGlobalApiHandlers
1693+ {
1694+ LlmInference = new LlmInferenceAdapter ( handler , ( ) => _serverRpc ) ,
1695+ } ;
1696+ }
1697+
1698+ /// <summary>
1699+ /// Tells the runtime to route its outbound model-layer requests through this
1700+ /// client's LLM inference provider. No-op when interception is not configured.
1701+ /// </summary>
1702+ private async Task ConfigureLlmInferenceAsync ( CancellationToken cancellationToken )
1703+ {
1704+ if ( _clientGlobalApis ? . LlmInference is null )
1705+ {
1706+ return ;
1707+ }
1708+
1709+ await Rpc . LlmInference . SetProviderAsync ( cancellationToken ) ;
1710+ }
1711+
16881712 private void ConfigureSessionFsHandlers ( CopilotSession session , Func < CopilotSession , SessionFsProvider > ? createSessionFsHandler )
16891713 {
16901714 if ( _options . SessionFs is null )
@@ -2079,6 +2103,10 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
20792103 var session = GetSession ( sessionId ) ?? throw new ArgumentException ( $ "Unknown session { sessionId } ") ;
20802104 return session . ClientSessionApis ;
20812105 } ) ;
2106+ if ( _clientGlobalApis is not null )
2107+ {
2108+ ClientGlobalApiRegistration . RegisterClientGlobalApiHandlers ( rpc , _clientGlobalApis ) ;
2109+ }
20822110 rpc . StartListening ( ) ;
20832111 LoggingHelpers . LogTiming ( _logger , LogLevel . Debug , null ,
20842112 "CopilotClient.ConnectToServerAsync transport setup complete. Elapsed={Elapsed}" ,
0 commit comments