Skip to content

Commit 65df1a1

Browse files
Port LLM inference callbacks to the .NET SDK
Mirrors the TypeScript LLM inference callback feature in the .NET SDK so consumers can observe/mutate the model-layer HTTP/WebSocket requests the runtime issues (CAPI and BYOK), with the runtime session id threaded into each callback. - scripts/codegen/csharp.ts: emit the clientGlobal handler interface + registration so Rpc.cs gains the llmInference handler surface. - LlmInferenceProvider.cs: low-level ILlmInferenceProvider API + adapter (request staging, response sink state machine) behind an internal ILlmInferenceResponseChannel seam for unit testing. - LlmRequestHandler.cs: idiomatic pass-through base class mapping to HttpRequestMessage/HttpResponseMessage and ClientWebSocket, with virtual transform/forward hooks for both transports. - Types.cs/Client.cs: wire LlmInferenceConfig into the client and register the provider on start. - Tests: factored unit-test infra (recording channel/sink, inline provider, frame builders) with adapter + handler tests, plus CAPI+BYOK e2e tests asserting the session id reaches the callback. e2e provider emits raw JSON (reflection-free STJ) and serves all model-layer traffic off-network. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f812ac2 commit 65df1a1

13 files changed

Lines changed: 2419 additions & 41 deletions

dotnet/src/Client.cs

Lines changed: 51 additions & 0 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);
@@ -1674,6 +1685,42 @@ await Rpc.SessionFs.SetProviderAsync(
16741685
cancellationToken: cancellationToken);
16751686
}
16761687

1688+
/// <summary>
1689+
/// Builds the client-global RPC handler bag at construction time. Currently
1690+
/// only the LLM inference provider adapter is registered; returns null when no
1691+
/// client-global API is configured so the registration is skipped entirely.
1692+
/// </summary>
1693+
private ClientGlobalApiHandlers? BuildClientGlobalApis()
1694+
{
1695+
var factory = _options.LlmInference?.CreateLlmInferenceProvider;
1696+
if (factory is null)
1697+
{
1698+
return null;
1699+
}
1700+
1701+
var provider = factory()
1702+
?? throw new InvalidOperationException("LlmInferenceConfig.CreateLlmInferenceProvider returned null.");
1703+
1704+
return new ClientGlobalApiHandlers
1705+
{
1706+
LlmInference = new LlmInferenceAdapter(provider, () => _serverRpc),
1707+
};
1708+
}
1709+
1710+
/// <summary>
1711+
/// Tells the runtime to route its outbound model-layer requests through this
1712+
/// client's LLM inference provider. No-op when interception is not configured.
1713+
/// </summary>
1714+
private async Task ConfigureLlmInferenceAsync(CancellationToken cancellationToken)
1715+
{
1716+
if (_clientGlobalApis?.LlmInference is null)
1717+
{
1718+
return;
1719+
}
1720+
1721+
await Rpc.LlmInference.SetProviderAsync(cancellationToken);
1722+
}
1723+
16771724
private void ConfigureSessionFsHandlers(CopilotSession session, Func<CopilotSession, SessionFsProvider>? createSessionFsHandler)
16781725
{
16791726
if (_options.SessionFs is null)
@@ -2067,6 +2114,10 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
20672114
var session = GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
20682115
return session.ClientSessionApis;
20692116
});
2117+
if (_clientGlobalApis is not null)
2118+
{
2119+
ClientGlobalApiRegistration.RegisterClientGlobalApiHandlers(rpc, _clientGlobalApis);
2120+
}
20702121
rpc.StartListening();
20712122
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
20722123
"CopilotClient.ConnectToServerAsync transport setup complete. Elapsed={Elapsed}",

dotnet/src/Generated/Rpc.cs

Lines changed: 267 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/GitHub.Copilot.SDK.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
<NoWarn>$(NoWarn);GHCP001</NoWarn>
2828
</PropertyGroup>
2929

30+
<ItemGroup>
31+
<InternalsVisibleTo Include="GitHub.Copilot.SDK.Test" />
32+
</ItemGroup>
33+
3034
<PropertyGroup Condition="'$(CI)' == 'true' or '$(TF_BUILD)' == 'true'">
3135
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
3236
</PropertyGroup>

0 commit comments

Comments
 (0)