Skip to content

Commit f00c17f

Browse files
Add sessionId to BYOK provider-token callback; rename to bearerTokenProvider (#1796)
1 parent 6d0e78f commit f00c17f

30 files changed

Lines changed: 302 additions & 221 deletions

dotnet/src/BearerTokenProvider.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace GitHub.Copilot;
88

99
/// <summary>
10-
/// Arguments passed to a bearer-token callback (the <c>GetBearerToken</c> property
10+
/// Arguments passed to a bearer-token callback (the <c>BearerTokenProvider</c> property
1111
/// on <see cref="ProviderConfig"/> / <see cref="NamedProviderConfig"/>) when the
1212
/// runtime needs a fresh bearer token for a BYOK provider.
1313
/// </summary>
@@ -29,4 +29,11 @@ public sealed class ProviderTokenArgs
2929
/// provider-agnostic and forwards only the provider name.
3030
/// </remarks>
3131
public required string ProviderName { get; init; }
32+
33+
/// <summary>
34+
/// Id of the session that triggered this token request. A client-level
35+
/// shared callback registered for many sessions can use this to resolve the
36+
/// owning session and scope token acquisition or caching per session.
37+
/// </summary>
38+
public required string SessionId { get; init; }
3239
}

dotnet/src/Client.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -671,26 +671,23 @@ private CopilotSession InitializeSession(
671671
private const string DefaultBearerTokenProviderName = "default";
672672

673673
/// <summary>
674-
/// Collects the per-provider <c>GetBearerToken</c> callbacks keyed by
674+
/// Collects the per-provider <c>BearerTokenProvider</c> callbacks keyed by
675675
/// provider name for session-side registration. The singular, whole-session
676676
/// <see cref="ProviderConfig"/> uses the implicit
677677
/// <see cref="DefaultBearerTokenProviderName"/>.
678678
/// </summary>
679679
private static Dictionary<string, Func<ProviderTokenArgs, Task<string>>> BuildBearerTokenCallbacks(SessionConfigBase config)
680680
{
681681
var callbacks = new Dictionary<string, Func<ProviderTokenArgs, Task<string>>>(StringComparer.Ordinal);
682-
if (config.Provider?.GetBearerToken is { } singular)
682+
if (config.Provider?.BearerTokenProvider is { } singular)
683683
{
684684
callbacks[DefaultBearerTokenProviderName] = singular;
685685
}
686686
if (config.Providers != null)
687687
{
688-
foreach (var provider in config.Providers)
688+
foreach (var provider in config.Providers.Where(provider => provider.BearerTokenProvider is not null))
689689
{
690-
if (provider.GetBearerToken is { } callback)
691-
{
692-
callbacks[provider.Name] = callback;
693-
}
690+
callbacks[provider.Name] = provider.BearerTokenProvider!;
694691
}
695692
}
696693
return callbacks;

dotnet/src/Session.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,7 @@ internal void RegisterAutoModeSwitchHandler(Func<AutoModeSwitchRequest, AutoMode
871871
}
872872

873873
/// <summary>
874-
/// Registers per-provider <c>GetBearerToken</c> callbacks for BYOK
874+
/// Registers per-provider <c>BearerTokenProvider</c> callbacks for BYOK
875875
/// providers configured with managed-identity / on-demand bearer-token auth.
876876
/// </summary>
877877
/// <remarks>
@@ -899,7 +899,7 @@ internal void RegisterBearerTokenProviders(IReadOnlyDictionary<string, Func<Prov
899899

900900
/// <summary>
901901
/// Routes runtime <c>providerToken.getToken</c> requests to the matching
902-
/// per-provider <c>GetBearerToken</c> callback registered on the session.
902+
/// per-provider <c>BearerTokenProvider</c> callback registered on the session.
903903
/// </summary>
904904
private sealed class BearerTokenProviderHandler(CopilotSession session) : IProviderTokenHandler
905905
{
@@ -910,7 +910,7 @@ public async Task<ProviderTokenAcquireResult> GetTokenAsync(ProviderTokenAcquire
910910
throw new InvalidOperationException(
911911
$"No bearer-token provider registered for provider \"{request.ProviderName}\"");
912912
}
913-
var token = await callback(new ProviderTokenArgs { ProviderName = request.ProviderName }).ConfigureAwait(false);
913+
var token = await callback(new ProviderTokenArgs { ProviderName = request.ProviderName, SessionId = request.SessionId }).ConfigureAwait(false);
914914
return new ProviderTokenAcquireResult { Token = token };
915915
}
916916
}

dotnet/src/Types.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,26 +2044,27 @@ public sealed class ProviderConfig
20442044
public string? BearerToken { get; set; }
20452045

20462046
/// <summary>
2047-
/// Wire-only flag, emitted automatically when <see cref="GetBearerToken"/> is set, that tells
2047+
/// Wire-only flag, emitted automatically when <see cref="BearerTokenProvider"/> is set, that tells
20482048
/// the runtime to request a token over the session-scoped <c>providerToken.getToken</c> RPC
2049-
/// before each outbound request to this provider. Derived from <see cref="GetBearerToken"/>;
2049+
/// before each outbound request to this provider. Derived from <see cref="BearerTokenProvider"/>;
20502050
/// internal and never part of the public API.
20512051
/// </summary>
20522052
[JsonInclude]
20532053
[JsonPropertyName("hasBearerTokenProvider")]
2054-
internal bool? HasBearerTokenProvider => GetBearerToken is not null ? true : null;
2054+
internal bool? HasBearerTokenProvider => BearerTokenProvider is not null ? true : null;
20552055

20562056
/// <summary>
20572057
/// Per-request callback that resolves a bearer token on demand for this BYOK provider (for
20582058
/// example via Azure Managed Identity). The Copilot SDK takes no identity dependency: supply a
20592059
/// callback backed by your own identity library. Never serialized — setting it makes the SDK send
20602060
/// <c>hasBearerTokenProvider: true</c> on the wire and answer the runtime's
2061-
/// <c>providerToken.getToken</c> requests. Mutually exclusive with <see cref="ApiKey"/> and
2062-
/// <see cref="BearerToken"/>.
2061+
/// <c>providerToken.getToken</c> requests. When set alongside <see cref="ApiKey"/>/<see cref="BearerToken"/>, this callback takes precedence:
2062+
/// the runtime applies the token it returns as the Authorization: Bearer header for each request
2063+
/// and does not send the static credential.
20632064
/// </summary>
20642065
[JsonIgnore]
20652066
[Experimental(Diagnostics.Experimental)]
2066-
public Func<ProviderTokenArgs, Task<string>>? GetBearerToken { get; set; }
2067+
public Func<ProviderTokenArgs, Task<string>>? BearerTokenProvider { get; set; }
20672068

20682069
/// <summary>
20692070
/// Azure-specific configuration options.
@@ -2198,26 +2199,27 @@ public sealed class NamedProviderConfig
21982199
public string? BearerToken { get; set; }
21992200

22002201
/// <summary>
2201-
/// Wire-only flag, emitted automatically when <see cref="GetBearerToken"/> is set, that tells
2202+
/// Wire-only flag, emitted automatically when <see cref="BearerTokenProvider"/> is set, that tells
22022203
/// the runtime to request a token over the session-scoped <c>providerToken.getToken</c> RPC
2203-
/// before each outbound request to this provider. Derived from <see cref="GetBearerToken"/>;
2204+
/// before each outbound request to this provider. Derived from <see cref="BearerTokenProvider"/>;
22042205
/// internal and never part of the public API.
22052206
/// </summary>
22062207
[JsonInclude]
22072208
[JsonPropertyName("hasBearerTokenProvider")]
2208-
internal bool? HasBearerTokenProvider => GetBearerToken is not null ? true : null;
2209+
internal bool? HasBearerTokenProvider => BearerTokenProvider is not null ? true : null;
22092210

22102211
/// <summary>
22112212
/// Per-request callback that resolves a bearer token on demand for this BYOK provider (for
22122213
/// example via Azure Managed Identity). The Copilot SDK takes no identity dependency: supply a
22132214
/// callback backed by your own identity library. Never serialized — setting it makes the SDK send
22142215
/// <c>hasBearerTokenProvider: true</c> on the wire and answer the runtime's
2215-
/// <c>providerToken.getToken</c> requests. Mutually exclusive with <see cref="ApiKey"/> and
2216-
/// <see cref="BearerToken"/>.
2216+
/// <c>providerToken.getToken</c> requests. When set alongside <see cref="ApiKey"/>/<see cref="BearerToken"/>, this callback takes precedence:
2217+
/// the runtime applies the token it returns as the Authorization: Bearer header for each request
2218+
/// and does not send the static credential.
22172219
/// </summary>
22182220
[JsonIgnore]
22192221
[Experimental(Diagnostics.Experimental)]
2220-
public Func<ProviderTokenArgs, Task<string>>? GetBearerToken { get; set; }
2222+
public Func<ProviderTokenArgs, Task<string>>? BearerTokenProvider { get; set; }
22212223

22222224
/// <summary>
22232225
/// Azure-specific configuration options.

dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace GitHub.Copilot.Test.E2E;
1313

1414
/// <summary>
1515
/// End-to-end coverage for the experimental BYOK bearer-token-provider surface
16-
/// (<c>GetBearerToken</c> on a provider config). The callback stays entirely on
16+
/// (<c>BearerTokenProvider</c> on a provider config). The callback stays entirely on
1717
/// the SDK/client side: the SDK strips it from the wire config, sets the
1818
/// <c>hasBearerTokenProvider</c> flag, and the runtime calls back over the
1919
/// session-scoped <c>providerToken.getToken</c> RPC before each outbound model
@@ -81,7 +81,7 @@ private static async Task RunTurnAsync(
8181
{
8282
await session.SendAndWaitAsync(new MessageOptions { Prompt = prompt });
8383
}
84-
catch
84+
catch (InvalidOperationException)
8585
{
8686
// The handler always 404s the BYOK endpoint, so the turn errors after
8787
// the token-bearing request was already captured. Expected.
@@ -110,7 +110,7 @@ public async Task Applies_The_Callbacks_Token_As_The_Authorization_Header()
110110
Type = "openai",
111111
WireApi = "completions",
112112
BaseUrl = PrimaryBaseUrl,
113-
GetBearerToken = _ =>
113+
BearerTokenProvider = _ =>
114114
{
115115
Interlocked.Increment(ref calls);
116116
return Task.FromResult(sentinel);
@@ -149,7 +149,7 @@ public async Task Re_Acquires_A_Fresh_Token_For_Each_Request()
149149
BaseUrl = PrimaryBaseUrl,
150150
// A distinct token per acquisition proves the runtime re-invokes
151151
// the callback per request rather than caching a previous token.
152-
GetBearerToken = _ =>
152+
BearerTokenProvider = _ =>
153153
{
154154
var n = Interlocked.Increment(ref calls);
155155
return Task.FromResult($"rotating-token-{n}");
@@ -189,6 +189,9 @@ Func<ProviderTokenArgs, Task<string>> MakeCallback(string providerName) =>
189189
// The runtime forwards the requesting provider's name so the client
190190
// can dispatch to the right credential.
191191
Assert.Equal(providerName, args.ProviderName);
192+
// The runtime also forwards the owning session id so a
193+
// client-level shared callback can resolve the session.
194+
Assert.False(string.IsNullOrEmpty(args.SessionId));
192195
acquiredFor.Add(providerName);
193196
return Task.FromResult(tokenByProvider[providerName]);
194197
};
@@ -205,15 +208,15 @@ Func<ProviderTokenArgs, Task<string>> MakeCallback(string providerName) =>
205208
Type = "openai",
206209
WireApi = "completions",
207210
BaseUrl = RedBaseUrl,
208-
GetBearerToken = MakeCallback("red"),
211+
BearerTokenProvider = MakeCallback("red"),
209212
},
210213
new()
211214
{
212215
Name = "blue",
213216
Type = "openai",
214217
WireApi = "completions",
215218
BaseUrl = BlueBaseUrl,
216-
GetBearerToken = MakeCallback("blue"),
219+
BearerTokenProvider = MakeCallback("blue"),
217220
},
218221
};
219222
var models = new List<ProviderModelConfig>
@@ -240,7 +243,7 @@ Func<ProviderTokenArgs, Task<string>> MakeCallback(string providerName) =>
240243
/// The runtime invokes <see cref="SendRequestAsync"/> for every model-layer HTTP
241244
/// request. Requests aimed at a fake BYOK host (<c>*.invalid</c>) are captured —
242245
/// recording the <c>Authorization</c> header the runtime applied after calling
243-
/// the provider's <c>GetBearerToken</c> callback over the session-scoped
246+
/// the provider's <c>BearerTokenProvider</c> callback over the session-scoped
244247
/// <c>providerToken.getToken</c> RPC — and answered with a synthetic <c>404</c>
245248
/// (a non-retryable status, so each outbound model request yields exactly one
246249
/// capture). Every other request (CAPI bootstrap: model catalog, policy, …) is
@@ -262,13 +265,14 @@ protected override Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage
262265
? string.Join(", ", values)
263266
: null));
264267

265-
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
268+
var response = new HttpResponseMessage(HttpStatusCode.NotFound)
266269
{
267270
Content = new StringContent(
268271
"{\"error\":{\"message\":\"fake byok endpoint\"}}",
269272
System.Text.Encoding.UTF8,
270273
"application/json"),
271-
});
274+
};
275+
return Task.FromResult(response);
272276
}
273277

274278
// CAPI bootstrap (model catalog, policy, …) — answered off-network.

go/client.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,18 @@ import (
5757
// whole-session [ProviderConfig]. Named providers are keyed by their own Name.
5858
const defaultBearerTokenProviderName = "default"
5959

60-
// collectBearerTokenProviders gathers the per-provider [GetBearerToken] callbacks
60+
// collectBearerTokenProviders gathers the per-provider [BearerTokenProvider] callbacks
6161
// from the singular provider and any named providers, keyed by provider name. The
6262
// singular provider uses the implicit name "default"; named providers use their
6363
// own Name. Returns nil when no callbacks are configured.
64-
func collectBearerTokenProviders(provider *ProviderConfig, providers []NamedProviderConfig) map[string]GetBearerToken {
65-
callbacks := make(map[string]GetBearerToken)
66-
if provider != nil && provider.GetBearerToken != nil {
67-
callbacks[defaultBearerTokenProviderName] = provider.GetBearerToken
64+
func collectBearerTokenProviders(provider *ProviderConfig, providers []NamedProviderConfig) map[string]BearerTokenProvider {
65+
callbacks := make(map[string]BearerTokenProvider)
66+
if provider != nil && provider.BearerTokenProvider != nil {
67+
callbacks[defaultBearerTokenProviderName] = provider.BearerTokenProvider
6868
}
6969
for i := range providers {
70-
if providers[i].GetBearerToken != nil {
71-
callbacks[providers[i].Name] = providers[i].GetBearerToken
70+
if providers[i].BearerTokenProvider != nil {
71+
callbacks[providers[i].Name] = providers[i].BearerTokenProvider
7272
}
7373
}
7474
if len(callbacks) == 0 {

go/internal/e2e/byok_bearer_token_provider_e2e_test.go

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type capturedBYOKRequest struct {
3737

3838
// byokCapturingRoundTripper stands in for a real HTTP upstream. It records the
3939
// `Authorization` header the runtime applied (after calling the provider's
40-
// GetBearerToken callback over the session-scoped `providerToken.getToken` RPC)
40+
// BearerTokenProvider callback over the session-scoped `providerToken.getToken` RPC)
4141
// for every request aimed at a fake `.invalid` BYOK host, answering them with a
4242
// synthetic 404 (a non-retryable status, so each outbound model request yields
4343
// exactly one capture). Every other request (CAPI bootstrap: model catalog,
@@ -96,7 +96,7 @@ func (rt *byokCapturingRoundTripper) reset() {
9696
}
9797

9898
// TestBYOKBearerTokenProvider is end-to-end coverage for the experimental BYOK
99-
// bearer-token-provider surface (GetBearerToken on a provider config). The
99+
// bearer-token-provider surface (BearerTokenProvider on a provider config). The
100100
// callback stays entirely on the SDK/client side: the SDK strips it from the
101101
// wire config, sets the `hasBearerTokenProvider` flag, and the runtime calls
102102
// back over the session-scoped `providerToken.getToken` RPC before each outbound
@@ -151,11 +151,11 @@ func TestBYOKBearerTokenProvider(t *testing.T) {
151151
}
152152

153153
providers := []copilot.NamedProviderConfig{{
154-
Name: "mi",
155-
Type: "openai",
156-
WireAPI: "completions",
157-
BaseURL: byokPrimaryBaseURL,
158-
GetBearerToken: getBearerToken,
154+
Name: "mi",
155+
Type: "openai",
156+
WireAPI: "completions",
157+
BaseURL: byokPrimaryBaseURL,
158+
BearerTokenProvider: getBearerToken,
159159
}}
160160
models := []copilot.ProviderModelConfig{{ID: "default", Provider: "mi", WireModel: "byok-gpt-4o"}}
161161

@@ -189,11 +189,11 @@ func TestBYOKBearerTokenProvider(t *testing.T) {
189189
}
190190

191191
providers := []copilot.NamedProviderConfig{{
192-
Name: "mi",
193-
Type: "openai",
194-
WireAPI: "completions",
195-
BaseURL: byokPrimaryBaseURL,
196-
GetBearerToken: getBearerToken,
192+
Name: "mi",
193+
Type: "openai",
194+
WireAPI: "completions",
195+
BaseURL: byokPrimaryBaseURL,
196+
BearerTokenProvider: getBearerToken,
197197
}}
198198
models := []copilot.ProviderModelConfig{{ID: "default", Provider: "mi", WireModel: "byok-gpt-4o"}}
199199

@@ -227,13 +227,18 @@ func TestBYOKBearerTokenProvider(t *testing.T) {
227227
}
228228
var mu sync.Mutex
229229
var acquiredFor []string
230-
makeCallback := func(providerName string) copilot.GetBearerToken {
230+
makeCallback := func(providerName string) copilot.BearerTokenProvider {
231231
return func(args copilot.ProviderTokenArgs) (string, error) {
232232
// The runtime forwards the requesting provider's name so the
233233
// client can dispatch to the right credential.
234234
if args.ProviderName != providerName {
235235
t.Errorf("Expected providerName %q, got %q", providerName, args.ProviderName)
236236
}
237+
// The runtime also forwards the owning session id so a
238+
// client-level shared callback can resolve the session.
239+
if args.SessionID == "" {
240+
t.Errorf("Expected a non-empty session id in token args")
241+
}
237242
mu.Lock()
238243
acquiredFor = append(acquiredFor, providerName)
239244
mu.Unlock()
@@ -243,18 +248,18 @@ func TestBYOKBearerTokenProvider(t *testing.T) {
243248

244249
providers := []copilot.NamedProviderConfig{
245250
{
246-
Name: "red",
247-
Type: "openai",
248-
WireAPI: "completions",
249-
BaseURL: byokRedBaseURL,
250-
GetBearerToken: makeCallback("red"),
251+
Name: "red",
252+
Type: "openai",
253+
WireAPI: "completions",
254+
BaseURL: byokRedBaseURL,
255+
BearerTokenProvider: makeCallback("red"),
251256
},
252257
{
253-
Name: "blue",
254-
Type: "openai",
255-
WireAPI: "completions",
256-
BaseURL: byokBlueBaseURL,
257-
GetBearerToken: makeCallback("blue"),
258+
Name: "blue",
259+
Type: "openai",
260+
WireAPI: "completions",
261+
BaseURL: byokBlueBaseURL,
262+
BearerTokenProvider: makeCallback("blue"),
258263
},
259264
}
260265
models := []copilot.ProviderModelConfig{

0 commit comments

Comments
 (0)