Add getBearerToken callback for BYOK providers (Managed Identity)#1748
Conversation
This comment has been minimized.
This comment has been minimized.
0ba9325 to
d321d4a
Compare
This comment has been minimized.
This comment has been minimized.
|
Thanks — a couple of these points are now moot after a contract simplification pushed since this review ran:
So the remaining cross-SDK surface is just (2) the |
13fd1d7 to
02fb2fc
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| # already sent the (token-bearing) request — which is all we assert on. | ||
| try: | ||
| await session.send_and_wait(prompt) | ||
| except Exception: |
This comment has been minimized.
This comment has been minimized.
| return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) | ||
| { | ||
| Content = new StringContent( | ||
| "{\"error\":{\"message\":\"fake byok endpoint\"}}", | ||
| System.Text.Encoding.UTF8, | ||
| "application/json"), | ||
| }); |
| foreach (var provider in config.Providers) | ||
| { | ||
| if (provider.GetBearerToken is { } callback) | ||
| { | ||
| callbacks[provider.Name] = callback; | ||
| } | ||
| } |
| catch | ||
| { | ||
| // The handler always 404s the BYOK endpoint, so the turn errors after | ||
| // the token-bearing request was already captured. Expected. | ||
| } |
| /// </summary> | ||
| [JsonIgnore] | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public Func<ProviderTokenArgs, Task<string>>? GetBearerToken { get; set; } |
There was a problem hiding this comment.
Two design concerns here that are worth addressing while this is still in draft:
Naming: GetBearerToken sitting directly below BearerToken reads as a variant of the same setting rather than a fundamentally different auth mode. In an object initializer, the difference between a static string and a callback is easy to miss. I'd suggest GetBearerTokenProvider — it signals "dynamic token source" rather than "another kind of bearer token value." I'm drawing inspiration from what was done for the OpenAI SDK, which uses https://github.com/Azure/azure-sdk-for-js/blob/a5cdd18b24553a0886bd59964bd9fad58bddb435/sdk/identity/identity/src/tokenProvider.ts#L48.
Mutual exclusion: All 3 auth options (ApiKey, BearerToken, BearerTokenProvider) are flat, independent, settable properties with no SDK-side guard. The docs mark them mutually exclusive, but nothing prevents a caller from setting 2 (or all 3) at once. Without a guard, which one wins is a runtime implementation detail the caller can't easily discover. Two alternatives worth considering:
- Add validation at session startup (throw
InvalidOperationExceptionif more than one is non-null) - Model auth as a single union-like property, e.g.
Authentication = new ApiKey(...) | new BearerToken(...) | new BearerTokenProvider(...)
dc7d02b to
0ff7ec7
Compare
There was a problem hiding this comment.
Pull request overview
Adds an experimental BYOK authentication surface across all SDKs by allowing consumers to supply a per-request bearer-token acquisition callback (getBearerToken / GetBearerToken / get_bearer_token). SDKs keep the callback client-side, send a wire flag (hasBearerTokenProvider), and answer the runtime’s session-scoped providerToken.getToken RPC so the runtime can apply Authorization: Bearer <token> per outbound provider request.
Changes:
- Introduces per-provider bearer-token callback types + wire flag plumbing, and runtime→SDK callback routing via
providerToken.getToken. - Wires callback registration into session create/resume flows (including per-provider dispatch by name, with
"default"for the singular provider). - Adds new E2E tests in each language validating header application, per-request acquisition, and per-provider routing.
Show a summary per file
| File | Description |
|---|---|
| rust/tests/e2e/byok_bearer_token_provider.rs | New Rust E2E coverage for bearer-token callback behavior via request interception. |
| rust/tests/e2e.rs | Registers the new Rust E2E module. |
| rust/src/types.rs | Adds get_bearer_token callback fields, wire flag handling, and collects providers into session runtime state. |
| rust/src/session.rs | Routes inbound providerToken.getToken JSON-RPC calls to provider-token dispatch. |
| rust/src/provider_token.rs | New Rust public callback trait/types (BearerTokenProvider, args, error). |
| rust/src/provider_token_dispatch.rs | New Rust dispatcher answering providerToken.getToken from registered callbacks. |
| rust/src/lib.rs | Exposes provider-token module/types and includes dispatch module. |
| python/e2e/test_byok_bearer_token_provider_e2e.py | New Python E2E tests for callback token application, per-request acquisition, and per-provider routing. |
| python/copilot/session.py | Adds Python callback types + session-side adapter to answer providerToken.getToken. |
| python/copilot/client.py | Collects callbacks, registers them on sessions, and emits hasBearerTokenProvider on the wire. |
| python/copilot/init.py | Exports GetBearerToken and ProviderTokenArgs. |
| nodejs/test/e2e/byok_bearer_token_provider.e2e.test.ts | New Node E2E tests for callback token behavior. |
| nodejs/src/types.ts | Adds Node public types (ProviderTokenArgs, GetBearerToken) and provider-config getBearerToken. |
| nodejs/src/session.ts | Registers bearer-token providers and implements session API handler for providerToken.getToken. |
| nodejs/src/index.ts | Re-exports the new Node public types. |
| nodejs/src/client.ts | Strips non-serializable callbacks from provider configs, emits wire flag, and registers callbacks on sessions. |
| java/src/test/java/com/github/copilot/ByokBearerTokenProviderE2ETest.java | New Java E2E tests validating token callback behavior. |
| java/src/main/java/com/github/copilot/SessionRequestBuilder.java | Collects bearer-token callbacks from configs and registers them on sessions. |
| java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java | Adds handler for providerToken.getToken inbound RPC. |
| java/src/main/java/com/github/copilot/rpc/ProviderTokenArgs.java | New Java callback args type (experimental). |
| java/src/main/java/com/github/copilot/rpc/ProviderConfig.java | Adds SDK-side bearer-token callback + emits hasBearerTokenProvider on the wire. |
| java/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java | Adds SDK-side bearer-token callback + emits hasBearerTokenProvider on the wire. |
| java/src/main/java/com/github/copilot/rpc/GetBearerToken.java | New Java functional interface for bearer-token acquisition. |
| java/src/main/java/com/github/copilot/CopilotSession.java | Stores per-session bearer-token callbacks keyed by provider name. |
| go/types.go | Adds Go callback types and custom JSON marshaling to emit hasBearerTokenProvider. |
| go/session.go | Registers per-session callbacks and answers providerToken.getToken via session API handler. |
| go/internal/e2e/byok_bearer_token_provider_e2e_test.go | New Go E2E tests validating token callback behavior. |
| go/client.go | Collects callbacks from config and registers them on sessions. |
| dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs | New .NET E2E tests validating token callback behavior. |
| dotnet/src/Types.cs | Adds .NET GetBearerToken callback + emits hasBearerTokenProvider on the wire. |
| dotnet/src/Session.cs | Registers callbacks per session and answers providerToken.getToken requests. |
| dotnet/src/Client.cs | Collects callbacks and registers them during session initialization. |
| dotnet/src/BearerTokenProvider.cs | Adds .NET ProviderTokenArgs type for callbacks. |
Copilot's findings
- Files reviewed: 33/33 changed files
- Comments generated: 9
| /** | ||
| * Gets the bearer-token provider callback. | ||
| * | ||
| * @return the bearer-token provider callback, or {@code null} if not set | ||
| */ | ||
| public GetBearerToken getGetBearerToken() { | ||
| return getBearerToken; |
| /** | ||
| * Gets the bearer-token provider callback. | ||
| * | ||
| * @return the bearer-token provider callback, or {@code null} if not set | ||
| */ | ||
| public GetBearerToken getGetBearerToken() { | ||
| return getBearerToken; |
This comment has been minimized.
This comment has been minimized.
Lets BYOK provider configs supply a getBearerToken callback so the SDK consumer resolves bearer tokens (e.g. Azure Managed Identity) on demand. The callback never crosses the wire: the SDK strips it from the provider config, sends a `hasBearerTokenProvider: true` flag, and answers the runtime's session-scoped `providerToken.getToken` RPC by routing to the matching per-provider callback. The returned token is applied as the Authorization header for outbound model requests; the consumer owns caching/refresh. Implemented across all SDKs (Node, .NET, Go, Java, Python, Rust) with e2e tests. The generated RPC files are intentionally left as the committed CLI 1.0.65 codegen output (providerToken.getToken + hasBearerTokenProvider) rather than hand-edited. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0ff7ec7 to
759bf6e
Compare
Cross-SDK Consistency Review ✅This PR ships the Feature parityAll six SDKs receive equivalent changes:
API naming consistency (language conventions respected)
Wire protocol consistencyAll SDKs strip the callback before serialization and emit Noteworthy language-appropriate design choices
No cross-SDK inconsistencies found. The implementation is thorough and well-aligned across all six languages.
|
… docs and CodeQL findings Address post-merge review feedback from #1748 across all 6 SDKs: - Rename the BYOK token callback field getBearerToken/get_bearer_token/ GetBearerToken to bearerTokenProvider/bearer_token_provider/ BearerTokenProvider, and the callback type to BearerTokenProvider. The Provider suffix distinguishes the dynamic token source from the static bearerToken credential and aligns with the existing Rust trait and the SDK's *Provider value-producer precedent. In Java this also drops the double-get accessor (getGetBearerToken -> getBearerTokenProvider). - Fix docs that incorrectly described the callback and static apiKey/ bearerToken as mutually exclusive; the runtime applies precedence (the callback wins and the static credential is not sent). - Resolve 4 CodeQL findings: empty except in python; LINQ .Where filter, specific catch type, and HttpResponseMessage disposal in dotnet. Java was not build-verified locally (requires JDK 25); CI validates it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Adds an experimental
getBearerTokencallback to BYOK provider configs so the SDK consumer can resolve bearer tokens (e.g. Azure Managed Identity via the consumer's identity library) on demand. The Copilot SDK takes zero Azure dependency — the consumer fills in the callback with whatever identity library they like. The runtime does no caching — it invokes the callback once per request, so the consumer (or the identity library it wraps) owns caching/refresh. Scope/audience is closed over by the callback and never crosses the wire.This PR ships the feature across all six SDKs — TypeScript/Node, .NET, Python, Go, Rust, and Java — each with an equivalent three-scenario e2e test.
Usage examples
Each example wires
DefaultAzureCredential(Azure Managed Identity) into the callback for thehttps://cognitiveservices.azure.com/.defaultscope. The identity library caches/refreshes the underlying token, so calling it per request is cheap.TypeScript / Node
.NET
Python
Go
Rust
Java