Skip to content

Commit 0667a46

Browse files
HTTP request callback support (#1689)
1 parent 0b117c1 commit 0667a46

71 files changed

Lines changed: 13475 additions & 155 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dotnet/src/Client.cs

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)