Skip to content

Commit 26ab612

Browse files
tclemCopilot
andcommitted
Merge main + parity-add ProviderConfig token/model overrides
Brought in 12 commits from origin/main, including CLI bumps to 1.0.41-0 and 1.0.41-1, plus upstream PR #966 ("Add provider model and token limit overrides to ProviderConfig"). One trivial codegen diff (single doc-comment update on `CustomAgentsUpdatedAgent.tools` for the new "or null when all tools are available" semantics). PR #966 added four new fields to ProviderConfig across all SDKs: - `model_id: Option<String>` (well-known model ID for agent config + token limit lookup; falls back to SessionConfig::model) - `wire_model: Option<String>` (model name sent to provider API for inference; falls back to model_id, then to SessionConfig::model) - `max_prompt_tokens: Option<i64>` (overrides resolved model's default max prompt tokens; triggers compaction) - `max_output_tokens: Option<i64>` (overrides resolved model's default max output tokens; truncates response) Plus matching `with_*` builders. Wire-shape: camelCase (`modelId`/`wireModel`/`maxPromptTokens`/`maxOutputTokens`), skip_serializing_if when unset. Extended `provider_config_builder_composes` test to exercise all four fields and assert their wire shape (camelCase + omission). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents ae7635d + 58cf64d commit 26ab612

167 files changed

Lines changed: 12033 additions & 1201 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.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ test/scenarios/**/rust/Cargo.lock
99

1010
# Visual Studio
1111
.vs/
12+
13+
# C# Dev Kit
14+
*.csproj.lscache

docs/auth/byok.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -475,16 +475,10 @@ When using BYOK, be aware of these limitations:
475475

476476
### Identity Limitations
477477

478-
BYOK authentication uses **static credentials only**. The following identity providers are NOT supported:
479-
480-
-**Microsoft Entra ID (Azure AD)** - No support for Entra managed identities or service principals
481-
-**Third-party identity providers** - No OIDC, SAML, or other federated identity
482-
-**Managed identities** - Azure Managed Identity is not supported
478+
BYOK authentication uses **static credentials only**.
483479

484480
You must use an API key or static bearer token that you manage yourself.
485481

486-
**Why not Entra ID?** While Entra ID does issue bearer tokens, these tokens are short-lived (typically 1 hour) and require automatic refresh via the Azure Identity SDK. The `bearerToken` option only accepts a static string—there is no callback mechanism for the SDK to request fresh tokens. For long-running workloads requiring Entra authentication, you would need to implement your own token refresh logic and create new sessions with updated tokens.
487-
488482
### Feature Limitations
489483

490484
Some Copilot features may behave differently with BYOK:

dotnet/src/Client.cs

Lines changed: 157 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -226,30 +226,46 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
226226
_logger.LogDebug("Starting Copilot client");
227227
_disconnected = false;
228228

229-
Task<Connection> result;
229+
Connection? connection = null;
230+
Process? cliProcess = null;
230231

231-
if (_optionsHost is not null && _optionsPort is not null)
232-
{
233-
// External server (TCP)
234-
_actualPort = _optionsPort;
235-
result = ConnectToServerAsync(null, _optionsHost, _optionsPort, null, ct);
236-
}
237-
else
232+
try
238233
{
239-
// Child process (stdio or TCP)
240-
var (cliProcess, portOrNull, stderrBuffer) = await StartCliServerAsync(_options, _effectiveConnectionToken, _logger, ct);
241-
_actualPort = portOrNull;
242-
result = ConnectToServerAsync(cliProcess, portOrNull is null ? null : "localhost", portOrNull, stderrBuffer, ct);
243-
}
234+
if (_optionsHost is not null && _optionsPort is not null)
235+
{
236+
// External server (TCP)
237+
_actualPort = _optionsPort;
238+
connection = await ConnectToServerAsync(null, _optionsHost, _optionsPort, null, ct);
239+
}
240+
else
241+
{
242+
// Child process (stdio or TCP)
243+
var (startedProcess, portOrNull, stderrBuffer) = await StartCliServerAsync(_options, _effectiveConnectionToken, _logger, ct);
244+
cliProcess = startedProcess;
245+
_actualPort = portOrNull;
246+
connection = await ConnectToServerAsync(cliProcess, portOrNull is null ? null : "localhost", portOrNull, stderrBuffer, ct);
247+
}
244248

245-
var connection = await result;
249+
// Verify protocol version compatibility
250+
await VerifyProtocolVersionAsync(connection, ct);
251+
await ConfigureSessionFsAsync(ct);
246252

247-
// Verify protocol version compatibility
248-
await VerifyProtocolVersionAsync(connection, ct);
249-
await ConfigureSessionFsAsync(ct);
253+
_logger.LogInformation("Copilot client connected");
254+
return connection;
255+
}
256+
catch
257+
{
258+
if (connection is not null)
259+
{
260+
await CleanupConnectionAsync(connection, errors: null);
261+
}
262+
else if (cliProcess is not null)
263+
{
264+
await CleanupCliProcessAsync(cliProcess, errors: null, _logger);
265+
}
250266

251-
_logger.LogInformation("Copilot client connected");
252-
return connection;
267+
throw;
268+
}
253269
}
254270
}
255271

@@ -353,11 +369,27 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
353369
return;
354370
}
355371

356-
var ctx = await _connectionTask;
372+
var connectionTask = _connectionTask;
357373
_connectionTask = null;
358374

375+
Connection ctx;
376+
try
377+
{
378+
ctx = await connectionTask;
379+
}
380+
catch (Exception ex)
381+
{
382+
_logger.LogDebug(ex, "Ignoring failed Copilot client startup during cleanup");
383+
return;
384+
}
385+
386+
await CleanupConnectionAsync(ctx, errors);
387+
}
388+
389+
private async Task CleanupConnectionAsync(Connection ctx, List<Exception>? errors)
390+
{
359391
try { ctx.Rpc.Dispose(); }
360-
catch (Exception ex) { errors?.Add(ex); }
392+
catch (Exception ex) { AddCleanupError(errors, ex, _logger); }
361393

362394
// Clear RPC and models cache
363395
_serverRpc = null;
@@ -366,17 +398,47 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
366398
if (ctx.NetworkStream is not null)
367399
{
368400
try { await ctx.NetworkStream.DisposeAsync(); }
369-
catch (Exception ex) { errors?.Add(ex); }
401+
catch (Exception ex) { AddCleanupError(errors, ex, _logger); }
370402
}
371403

372404
if (ctx.CliProcess is { } childProcess)
405+
{
406+
await CleanupCliProcessAsync(childProcess, errors, _logger);
407+
}
408+
}
409+
410+
private static async Task CleanupCliProcessAsync(Process childProcess, List<Exception>? errors, ILogger? logger)
411+
{
412+
try
373413
{
374414
try
375415
{
376-
if (!childProcess.HasExited) childProcess.Kill();
416+
if (!childProcess.HasExited)
417+
{
418+
childProcess.Kill(entireProcessTree: true);
419+
await childProcess.WaitForExitAsync();
420+
}
421+
}
422+
finally
423+
{
377424
childProcess.Dispose();
378425
}
379-
catch (Exception ex) { errors?.Add(ex); }
426+
}
427+
catch (Exception ex)
428+
{
429+
AddCleanupError(errors, ex, logger);
430+
}
431+
}
432+
433+
private static void AddCleanupError(List<Exception>? errors, Exception ex, ILogger? logger)
434+
{
435+
if (errors is not null)
436+
{
437+
errors.Add(ex);
438+
}
439+
else
440+
{
441+
logger?.LogDebug(ex, "Error while cleaning up Copilot CLI connection");
380442
}
381443
}
382444

@@ -1090,7 +1152,7 @@ internal static async Task<T> InvokeRpcAsync<T>(JsonRpc rpc, string method, obje
10901152

10911153
if (!string.IsNullOrEmpty(stderrOutput))
10921154
{
1093-
throw new IOException($"CLI process exited unexpectedly.\nstderr: {stderrOutput}", ex);
1155+
throw new IOException(FormatCliExitedMessage("CLI process exited unexpectedly.", stderrOutput), ex);
10941156
}
10951157
throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex);
10961158
}
@@ -1100,6 +1162,24 @@ internal static async Task<T> InvokeRpcAsync<T>(JsonRpc rpc, string method, obje
11001162
}
11011163
}
11021164

1165+
private static string FormatCliExitedMessage(string message, string stderrOutput)
1166+
{
1167+
return string.IsNullOrEmpty(stderrOutput)
1168+
? message
1169+
: $"{message}\nstderr: {stderrOutput}";
1170+
}
1171+
1172+
private static IOException CreateCliExitedException(string message, StringBuilder stderrBuffer)
1173+
{
1174+
string stderrOutput;
1175+
lock (stderrBuffer)
1176+
{
1177+
stderrOutput = stderrBuffer.ToString().Trim();
1178+
}
1179+
1180+
return new IOException(FormatCliExitedMessage(message, stderrOutput));
1181+
}
1182+
11031183
private Task<Connection> EnsureConnectedAsync(CancellationToken cancellationToken)
11041184
{
11051185
if (_connectionTask is null && !_options.AutoStart)
@@ -1152,7 +1232,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
11521232
connection.Rpc, "connect", [new ConnectRequest { Token = _effectiveConnectionToken }], connection.StderrBuffer, cancellationToken);
11531233
serverVersion = (int)connectResponse.ProtocolVersion;
11541234
}
1155-
catch (RemoteRpcException ex) when (ex.ErrorCode == RemoteRpcException.MethodNotFoundErrorCode)
1235+
catch (IOException ex) when (ex.InnerException is RemoteRpcException remoteEx && IsUnsupportedConnectMethod(remoteEx))
11561236
{
11571237
// Legacy server without `connect`; fall back to `ping`. A token, if any,
11581238
// is silently dropped — the legacy server can't enforce one.
@@ -1180,6 +1260,12 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
11801260
_negotiatedProtocolVersion = serverVersion.Value;
11811261
}
11821262

1263+
private static bool IsUnsupportedConnectMethod(RemoteRpcException ex)
1264+
{
1265+
return ex.ErrorCode == RemoteRpcException.MethodNotFoundErrorCode
1266+
|| string.Equals(ex.Message, "Unhandled method connect", StringComparison.Ordinal);
1267+
}
1268+
11831269
private static async Task<(Process Process, int? DetectedLocalhostTcpPort, StringBuilder StderrBuffer)> StartCliServerAsync(CopilotClientOptions options, string? connectionToken, ILogger logger, CancellationToken cancellationToken)
11841270
{
11851271
// Use explicit path, COPILOT_CLI_PATH env var (from options.Environment or process env), or bundled CLI - no PATH fallback
@@ -1277,18 +1363,24 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
12771363
if (telemetry.CaptureContent is { } capture) startInfo.Environment["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = capture ? "true" : "false";
12781364
}
12791365

1280-
var cliProcess = new Process { StartInfo = startInfo };
1281-
cliProcess.Start();
1282-
1283-
// Capture stderr for error messages and forward to logger
1284-
var stderrBuffer = new StringBuilder();
1285-
_ = Task.Run(async () =>
1366+
Process? cliProcess = null;
1367+
try
12861368
{
1287-
while (cliProcess != null && !cliProcess.HasExited)
1369+
cliProcess = new Process { StartInfo = startInfo };
1370+
cliProcess.Start();
1371+
1372+
// Capture stderr for error messages and forward to logger
1373+
var stderrBuffer = new StringBuilder();
1374+
var stderrReader = Task.Run(async () =>
12881375
{
1289-
var line = await cliProcess.StandardError.ReadLineAsync(cancellationToken);
1290-
if (line != null)
1376+
while (true)
12911377
{
1378+
var line = await cliProcess.StandardError.ReadLineAsync(cancellationToken);
1379+
if (line is null)
1380+
{
1381+
break;
1382+
}
1383+
12921384
lock (stderrBuffer)
12931385
{
12941386
stderrBuffer.AppendLine(line);
@@ -1299,28 +1391,43 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
12991391
logger.LogDebug("[CLI] {Line}", line);
13001392
}
13011393
}
1302-
}
1303-
}, cancellationToken);
1394+
}, cancellationToken);
13041395

1305-
var detectedLocalhostTcpPort = (int?)null;
1306-
if (options.UseStdio != true)
1307-
{
1308-
// Wait for port announcement
1309-
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
1310-
cts.CancelAfter(TimeSpan.FromSeconds(30));
1311-
1312-
while (!cts.Token.IsCancellationRequested)
1396+
var detectedLocalhostTcpPort = (int?)null;
1397+
if (options.UseStdio != true)
13131398
{
1314-
var line = await cliProcess.StandardOutput.ReadLineAsync(cts.Token) ?? throw new IOException("CLI process exited unexpectedly");
1315-
if (ListeningOnPortRegex().Match(line) is { Success: true } match)
1399+
// Wait for port announcement
1400+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
1401+
cts.CancelAfter(TimeSpan.FromSeconds(30));
1402+
1403+
while (!cts.Token.IsCancellationRequested)
13161404
{
1317-
detectedLocalhostTcpPort = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
1318-
break;
1405+
var line = await cliProcess.StandardOutput.ReadLineAsync(cts.Token);
1406+
if (line is null)
1407+
{
1408+
await stderrReader;
1409+
throw CreateCliExitedException("CLI process exited unexpectedly", stderrBuffer);
1410+
}
1411+
1412+
if (ListeningOnPortRegex().Match(line) is { Success: true } match)
1413+
{
1414+
detectedLocalhostTcpPort = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
1415+
break;
1416+
}
13191417
}
13201418
}
1419+
1420+
return (cliProcess, detectedLocalhostTcpPort, stderrBuffer);
13211421
}
1422+
catch
1423+
{
1424+
if (cliProcess is not null)
1425+
{
1426+
await CleanupCliProcessAsync(cliProcess, errors: null, logger);
1427+
}
13221428

1323-
return (cliProcess, detectedLocalhostTcpPort, stderrBuffer);
1429+
throw;
1430+
}
13241431
}
13251432

13261433
private static string? GetBundledCliPath(out string searchedPath)

dotnet/src/Generated/SessionEvents.cs

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

dotnet/src/Types.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,40 @@ public class ProviderConfig
15281528
/// </summary>
15291529
[JsonPropertyName("headers")]
15301530
public IDictionary<string, string>? Headers { get; set; }
1531+
1532+
/// <summary>
1533+
/// Well-known model name used by the runtime to look up agent configuration
1534+
/// (tools, prompts, reasoning behavior) and default token limits. Also used
1535+
/// as the wire model when <see cref="WireModel"/> is not set.
1536+
/// Falls back to <see cref="SessionConfig.Model"/>.
1537+
/// </summary>
1538+
[JsonPropertyName("modelId")]
1539+
public string? ModelId { get; set; }
1540+
1541+
/// <summary>
1542+
/// Model name sent to the provider API for inference. Use this when the
1543+
/// provider's model name (e.g. an Azure deployment name or a custom
1544+
/// fine-tune name) differs from <see cref="ModelId"/>.
1545+
/// Falls back to <see cref="ModelId"/>, then <see cref="SessionConfig.Model"/>.
1546+
/// </summary>
1547+
[JsonPropertyName("wireModel")]
1548+
public string? WireModel { get; set; }
1549+
1550+
/// <summary>
1551+
/// Overrides the resolved model's default max prompt tokens. The runtime
1552+
/// triggers conversation compaction before sending a request when the
1553+
/// prompt (system message, history, tool definitions, user message) would
1554+
/// exceed this limit.
1555+
/// </summary>
1556+
[JsonPropertyName("maxPromptTokens")]
1557+
public int? MaxInputTokens { get; set; }
1558+
1559+
/// <summary>
1560+
/// Overrides the resolved model's default max output tokens. When hit, the
1561+
/// model stops generating and returns a truncated response.
1562+
/// </summary>
1563+
[JsonPropertyName("maxOutputTokens")]
1564+
public int? MaxOutputTokens { get; set; }
15311565
}
15321566

15331567
/// <summary>

0 commit comments

Comments
 (0)