Skip to content

Add getBearerToken callback for BYOK providers (Managed Identity)#1748

Merged
stephentoub merged 1 commit into
mainfrom
stevesandersonms/byok-provider-token-rpc
Jun 25, 2026
Merged

Add getBearerToken callback for BYOK providers (Managed Identity)#1748
stephentoub merged 1 commit into
mainfrom
stevesandersonms/byok-provider-token-rpc

Conversation

@SteveSandersonMS

@SteveSandersonMS SteveSandersonMS commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an experimental getBearerToken callback 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 the https://cognitiveservices.azure.com/.default scope. The identity library caches/refreshes the underlying token, so calling it per request is cheap.

TypeScript / Node

import { DefaultAzureCredential } from "@azure/identity";
import type { GetBearerToken, ProviderConfig } from "@github/copilot";

const cred = new DefaultAzureCredential();

const provider: ProviderConfig = {
    type: "openai",
    baseUrl,
    getBearerToken: async () => (await cred.getToken()).token,
};

.NET

using Azure.Core;
using Azure.Identity;

var cred = new DefaultAzureCredential();

var provider = new ProviderConfig
{
    Type = "openai",
    BaseUrl = baseUrl,
    GetBearerToken = async args => (await cred.GetTokenAsync()).Token,
};

Python

from azure.identity.aio import DefaultAzureCredential

cred = DefaultAzureCredential()

async def get_bearer_token(args) -> str:
    token = await cred.get_token()
    return token.token

provider = {
    "type": "openai",
    "base_url": base_url,
    "get_bearer_token": get_bearer_token,
}

Go

import (
    "context"
    "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)

cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
    return err
}
scope := "https://cognitiveservices.azure.com/.default"

provider := copilot.ProviderConfig{
    Type:    "openai",
    BaseURL: baseURL,
    GetBearerToken: func(args copilot.ProviderTokenArgs) (string, error) {
        tok, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{
            Scopes: []string{scope},
        })
        if err != nil {
            return "", err
        }
        return tok.Token, nil
    },
}

Rust

use std::sync::Arc;
use azure_identity::DefaultAzureCredential;
use copilot::{ProviderConfig, ProviderTokenArgs};

let cred = DefaultAzureCredential::new()?;
let scope = "https://cognitiveservices.azure.com/.default";

let provider = ProviderConfig::new(base_url)
    .with_provider_type("openai")
    .with_get_bearer_token(Arc::new(move |_args: ProviderTokenArgs| {
        let cred = cred.clone();
        async move {
            let token = cred
                .get_token(&[scope])
                .await
                .map_err(|e| BearerTokenError::new(e.to_string()))?;
            Ok(token.token.secret().to_string())
        }
    }));

Java

import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.core.credential.TokenRequestContext;

var cred = new DefaultAzureCredentialBuilder().build();
var ctx = new TokenRequestContext().addScopes("https://cognitiveservices.azure.com/.default");

var provider = new ProviderConfig()
    .setType("openai")
    .setBaseUrl(baseUrl)
    .setGetBearerToken(args ->
        cred.getToken(ctx).map(t -> t.getToken()).toFuture());

@github-actions

This comment has been minimized.

@SteveSandersonMS SteveSandersonMS force-pushed the stevesandersonms/byok-provider-token-rpc branch from 0ba9325 to d321d4a Compare June 22, 2026 20:04
@github-actions

This comment has been minimized.

@SteveSandersonMS

Copy link
Copy Markdown
Contributor Author

Thanks — a couple of these points are now moot after a contract simplification pushed since this review ran:

  • bearerTokenScope has been removed entirely from the wire contract and from both provider configs. Scope/audience is closed over by the consumer''s getBearerToken callback (e.g. @azure/identity already binds the scope at getToken(scope) time), so it never needs to cross the wire. Point 1 no longer applies to any SDK.
  • The runtime now does no token cachingexpiresOnTimestamp was also dropped from the wire result; the callback owns caching/refresh.

So the remaining cross-SDK surface is just (2) the getBearerToken callback and (3) the providerToken.acquire server→client handler. This is intentionally Node-first and @experimental; parity for Python/Go/.NET/Java/Rust is planned as follow-up, not part of this PR. I''ll open tracking issues for the other SDKs once the Node + runtime contract lands.

@SteveSandersonMS SteveSandersonMS force-pushed the stevesandersonms/byok-provider-token-rpc branch from 13fd1d7 to 02fb2fc Compare June 23, 2026 10:08
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

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:
@github-actions

This comment has been minimized.

Comment on lines +265 to +271
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent(
"{\"error\":{\"message\":\"fake byok endpoint\"}}",
System.Text.Encoding.UTF8,
"application/json"),
});
Comment thread dotnet/src/Client.cs
Comment on lines +688 to +694
foreach (var provider in config.Providers)
{
if (provider.GetBearerToken is { } callback)
{
callbacks[provider.Name] = callback;
}
}
Comment on lines +84 to +88
catch
{
// The handler always 404s the BYOK endpoint, so the turn errors after
// the token-bearing request was already captured. Expected.
}
Comment thread dotnet/src/Types.cs
/// </summary>
[JsonIgnore]
[Experimental(Diagnostics.Experimental)]
public Func<ProviderTokenArgs, Task<string>>? GetBearerToken { get; set; }

@scottaddie scottaddie Jun 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 InvalidOperationException if more than one is non-null)
  • Model auth as a single union-like property, e.g. Authentication = new ApiKey(...) | new BearerToken(...) | new BearerTokenProvider(...)

@stephentoub stephentoub force-pushed the stevesandersonms/byok-provider-token-rpc branch from dc7d02b to 0ff7ec7 Compare June 25, 2026 01:30
@stephentoub stephentoub marked this pull request as ready for review June 25, 2026 01:31
@stephentoub stephentoub requested a review from a team as a code owner June 25, 2026 01:31
Copilot AI review requested due to automatic review settings June 25, 2026 01:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread nodejs/src/types.ts
Comment thread nodejs/src/types.ts
Comment thread python/copilot/session.py
Comment thread python/copilot/session.py
Comment thread dotnet/src/Types.cs
Comment thread dotnet/src/Types.cs
Comment on lines +228 to +234
/**
* Gets the bearer-token provider callback.
*
* @return the bearer-token provider callback, or {@code null} if not set
*/
public GetBearerToken getGetBearerToken() {
return getBearerToken;
Comment on lines +219 to +225
/**
* Gets the bearer-token provider callback.
*
* @return the bearer-token provider callback, or {@code null} if not set
*/
public GetBearerToken getGetBearerToken() {
return getBearerToken;
@github-actions

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>
@stephentoub stephentoub force-pushed the stevesandersonms/byok-provider-token-rpc branch from 0ff7ec7 to 759bf6e Compare June 25, 2026 02:01
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR ships the getBearerToken callback across all six SDKs simultaneously. I reviewed the implementation for cross-language consistency.

Feature parity

All six SDKs receive equivalent changes:

  • ProviderTokenArgs type (arguments passed to the callback)
  • GetBearerToken callback type/interface
  • getBearerToken field on both ProviderConfig and NamedProviderConfig
  • Session-side providerToken.getToken RPC handler
  • E2E tests with equivalent scenarios

API naming consistency (language conventions respected)

SDK Callback field Args type Callback type
TypeScript getBearerToken? ProviderTokenArgs (args) => Promise<string>
Python get_bearer_token ProviderTokenArgs Callable[..., str | Awaitable[str]]
Go GetBearerToken ProviderTokenArgs func(ProviderTokenArgs) (string, error)
.NET GetBearerToken ProviderTokenArgs Func<ProviderTokenArgs, Task<string>>
Java setGetBearerToken() ProviderTokenArgs GetBearerToken (functional interface)
Rust with_get_bearer_token() ProviderTokenArgs Arc<dyn BearerTokenProvider>

Wire protocol consistency

All SDKs strip the callback before serialization and emit hasBearerTokenProvider: true on the wire — each using the idiom natural to that language (custom MarshalJSON in Go, computed property in .NET, prepare_bearer_token_providers helper in Rust, etc.).

Noteworthy language-appropriate design choices

  • Python: Accepts both sync and async callbacks (str | Awaitable[str]), appropriate since Python consumers use both def and async def freely.
  • Rust: Uses a BearerTokenProvider trait + BearerTokenError type rather than a bare function, with a blanket impl for closures — idiomatic for Rust's async trait ecosystem.
  • Java: The getter is named getGetBearerToken() (double "get") due to JavaBean conventions on a field named getBearerToken. The setter setGetBearerToken() is the primary consumer-facing API and aligns with other SDKs' naming.

No cross-SDK inconsistencies found. The implementation is thorough and well-aligned across all six languages.

Generated by SDK Consistency Review Agent for issue #1748 · sonnet46 2M ·

@stephentoub stephentoub added this pull request to the merge queue Jun 25, 2026
Merged via the queue into main with commit e7d2bd3 Jun 25, 2026
41 checks passed
@stephentoub stephentoub deleted the stevesandersonms/byok-provider-token-rpc branch June 25, 2026 02:28
SteveSandersonMS added a commit that referenced this pull request Jun 25, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants