elicitationHandler = new AtomicReference<>();
@@ -1348,6 +1350,33 @@ void registerElicitationHandler(ElicitationHandler handler) {
elicitationHandler.set(handler);
}
+ /**
+ * Registers bearer-token provider callbacks for this session.
+ *
+ * Called internally when creating or resuming a session with BYOK providers
+ * that use managed-identity token callbacks.
+ *
+ * @param providers
+ * the callbacks keyed by provider name
+ */
+ void registerBearerTokenProviders(Map providers) {
+ bearerTokenProviders.clear();
+ if (providers != null) {
+ bearerTokenProviders.putAll(providers);
+ }
+ }
+
+ /**
+ * Gets the bearer-token provider callback for the given provider name.
+ *
+ * @param providerName
+ * the provider name
+ * @return the registered callback, or {@code null} if none is registered
+ */
+ GetBearerToken getBearerTokenProvider(String providerName) {
+ return bearerTokenProviders.get(providerName);
+ }
+
/**
* Registers an exit-plan-mode handler for this session.
*
diff --git a/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java b/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java
index 391f270db..b62e8c582 100644
--- a/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java
+++ b/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java
@@ -19,6 +19,8 @@
import com.github.copilot.generated.SessionEvent;
import com.github.copilot.rpc.AutoModeSwitchRequest;
import com.github.copilot.rpc.ExitPlanModeRequest;
+import com.github.copilot.rpc.GetBearerToken;
+import com.github.copilot.rpc.ProviderTokenArgs;
import com.github.copilot.rpc.PermissionRequestResult;
import com.github.copilot.rpc.PermissionRequestResultKind;
import com.github.copilot.rpc.SessionLifecycleEvent;
@@ -88,6 +90,8 @@ void registerHandlers(JsonRpcClient rpc) {
rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params));
rpc.registerMethodHandler("systemMessage.transform",
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
+ rpc.registerMethodHandler("providerToken.getToken",
+ (requestId, params) -> handleProviderTokenGetToken(rpc, requestId, params));
}
private void handleSessionEvent(JsonNode params) {
@@ -300,6 +304,68 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod
});
}
+ private void handleProviderTokenGetToken(JsonRpcClient rpc, String requestId, JsonNode params) {
+ LOG.fine("Received providerToken.getToken: " + params);
+ runAsync(() -> {
+ final long requestIdLong = parseRequestId(requestId, "providerToken.getToken");
+ if (requestIdLong == -1) {
+ return;
+ }
+ try {
+ String sessionId = params.get("sessionId").asText();
+ String providerName = params.get("providerName").asText();
+
+ CopilotSession session = sessions.get(sessionId);
+ if (session == null) {
+ rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId);
+ return;
+ }
+
+ GetBearerToken provider = session.getBearerTokenProvider(providerName);
+ if (provider == null) {
+ rpc.sendErrorResponse(requestIdLong, -32603,
+ "No bearer-token provider registered for provider " + providerName);
+ return;
+ }
+
+ CompletableFuture tokenFuture = provider.getToken(new ProviderTokenArgs(providerName));
+ if (tokenFuture == null) {
+ rpc.sendErrorResponse(requestIdLong, -32603,
+ "Bearer-token provider returned null future for provider " + providerName);
+ return;
+ }
+
+ tokenFuture.thenAccept(token -> {
+ try {
+ if (token == null) {
+ rpc.sendErrorResponse(requestIdLong, -32603,
+ "Bearer-token provider returned null token for provider " + providerName);
+ return;
+ }
+ rpc.sendResponse(requestIdLong, Map.of("token", token));
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending provider token response", e);
+ }
+ }).exceptionally(ex -> {
+ LOG.log(Level.WARNING, "Bearer-token provider exception", ex);
+ try {
+ rpc.sendErrorResponse(requestIdLong, -32603, "Bearer-token provider error: " + ex.getMessage());
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending provider token error", e);
+ }
+ return null;
+ });
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error handling providerToken.getToken", e);
+ try {
+ rpc.sendErrorResponse(requestIdLong, -32603, "Provider token handler error: " + e.getMessage());
+ } catch (IOException ioException) {
+ LOG.log(Level.SEVERE, "Error sending provider token handler error", ioException);
+ }
+ }
+ });
+ }
+
private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
runAsync(() -> {
final long requestIdLong = parseRequestId(requestId, "exitPlanMode.request");
diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java
index c26548a2f..073628945 100644
--- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java
+++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java
@@ -5,11 +5,15 @@
package com.github.copilot;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import com.github.copilot.rpc.CreateSessionRequest;
+import com.github.copilot.rpc.ProviderConfig;
+import com.github.copilot.rpc.NamedProviderConfig;
+import com.github.copilot.rpc.GetBearerToken;
import com.github.copilot.rpc.CommandWireDefinition;
import com.github.copilot.rpc.ResumeSessionConfig;
import com.github.copilot.rpc.ResumeSessionRequest;
@@ -329,6 +333,11 @@ static void configureSession(CopilotSession session, SessionConfig config) {
if (config.getOnElicitationRequest() != null) {
session.registerElicitationHandler(config.getOnElicitationRequest());
}
+ Map bearerTokenProviders = collectBearerTokenProviders(config.getProvider(),
+ config.getProviders());
+ if (!bearerTokenProviders.isEmpty()) {
+ session.registerBearerTokenProviders(bearerTokenProviders);
+ }
if (config.getOnExitPlanMode() != null) {
session.registerExitPlanModeHandler(config.getOnExitPlanMode());
}
@@ -371,6 +380,11 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config)
if (config.getOnElicitationRequest() != null) {
session.registerElicitationHandler(config.getOnElicitationRequest());
}
+ Map bearerTokenProviders = collectBearerTokenProviders(config.getProvider(),
+ config.getProviders());
+ if (!bearerTokenProviders.isEmpty()) {
+ session.registerBearerTokenProviders(bearerTokenProviders);
+ }
if (config.getOnExitPlanMode() != null) {
session.registerExitPlanModeHandler(config.getOnExitPlanMode());
}
@@ -381,4 +395,21 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config)
session.on(config.getOnEvent());
}
}
+
+ private static Map collectBearerTokenProviders(ProviderConfig provider,
+ List providers) {
+ Map bearerTokenProviders = new HashMap<>();
+ if (provider != null && provider.getGetBearerToken() != null) {
+ bearerTokenProviders.put("default", provider.getGetBearerToken());
+ }
+ if (providers != null) {
+ for (NamedProviderConfig namedProvider : providers) {
+ if (namedProvider != null && namedProvider.getName() != null
+ && namedProvider.getGetBearerToken() != null) {
+ bearerTokenProviders.put(namedProvider.getName(), namedProvider.getGetBearerToken());
+ }
+ }
+ }
+ return bearerTokenProviders;
+ }
}
diff --git a/java/src/main/java/com/github/copilot/rpc/GetBearerToken.java b/java/src/main/java/com/github/copilot/rpc/GetBearerToken.java
new file mode 100644
index 000000000..27ec7f09c
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/rpc/GetBearerToken.java
@@ -0,0 +1,40 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import java.util.concurrent.CompletableFuture;
+
+import com.github.copilot.CopilotExperimental;
+
+/**
+ * Functional interface for supplying per-provider bearer tokens for BYOK
+ * provider requests.
+ *
+ * The callback returns the raw token without a {@code Bearer } prefix. The SDK
+ * keeps this callback client-side and the runtime requests a token via the
+ * session-scoped {@code providerToken.getToken} RPC before each outbound model
+ * request.
+ *
+ * Experimental. This managed-identity surface may change or be
+ * removed in future SDK or CLI releases.
+ *
+ * @see ProviderConfig#setGetBearerToken(GetBearerToken)
+ * @see NamedProviderConfig#setGetBearerToken(GetBearerToken)
+ * @since 1.0.0
+ */
+@CopilotExperimental
+@FunctionalInterface
+public interface GetBearerToken {
+
+ /**
+ * Gets a bearer token for the provider identified by {@code args}.
+ *
+ * @param args
+ * the provider token request arguments
+ * @return a future that completes with the raw token, without a {@code Bearer }
+ * prefix
+ */
+ CompletableFuture getToken(ProviderTokenArgs args);
+}
diff --git a/java/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java b/java/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java
index dbc157739..2bdf2678f 100644
--- a/java/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java
+++ b/java/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java
@@ -7,6 +7,7 @@
import java.util.Collections;
import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -59,6 +60,9 @@ public class NamedProviderConfig {
@JsonProperty("bearerToken")
private String bearerToken;
+ @JsonIgnore
+ private GetBearerToken getBearerToken;
+
@JsonProperty("azure")
private AzureOptions azure;
@@ -212,6 +216,39 @@ public NamedProviderConfig setBearerToken(String bearerToken) {
return this;
}
+ /**
+ * Gets the bearer-token provider callback.
+ *
+ * @return the bearer-token provider callback, or {@code null} if not set
+ */
+ public GetBearerToken getGetBearerToken() {
+ return getBearerToken;
+ }
+
+ /**
+ * Sets a callback that supplies bearer tokens for outbound provider requests.
+ *
+ * Experimental. The callback stays SDK-side and is not
+ * serialized. Instead, the runtime receives a {@code hasBearerTokenProvider}
+ * flag and calls back over the session-scoped {@code providerToken.getToken}
+ * RPC before each model request. Return the raw token without a {@code Bearer }
+ * prefix.
+ *
+ * @param getBearerToken
+ * the bearer-token provider callback
+ * @return this config for method chaining
+ */
+ public NamedProviderConfig setGetBearerToken(GetBearerToken getBearerToken) {
+ this.getBearerToken = getBearerToken;
+ return this;
+ }
+
+ @JsonProperty("hasBearerTokenProvider")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ Boolean hasBearerTokenProviderWireFlag() {
+ return getBearerToken != null ? Boolean.TRUE : null;
+ }
+
/**
* Gets the Azure-specific options.
*
diff --git a/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java b/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java
index 8ba492ed9..ae59e7ead 100644
--- a/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java
+++ b/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java
@@ -56,6 +56,9 @@ public class ProviderConfig {
@JsonProperty("bearerToken")
private String bearerToken;
+ @JsonIgnore
+ private GetBearerToken getBearerToken;
+
@JsonProperty("azure")
private AzureOptions azure;
@@ -222,6 +225,39 @@ public ProviderConfig setBearerToken(String bearerToken) {
return this;
}
+ /**
+ * Gets the bearer-token provider callback.
+ *
+ * @return the bearer-token provider callback, or {@code null} if not set
+ */
+ public GetBearerToken getGetBearerToken() {
+ return getBearerToken;
+ }
+
+ /**
+ * Sets a callback that supplies bearer tokens for outbound provider requests.
+ *
+ * Experimental. The callback stays SDK-side and is not
+ * serialized. Instead, the runtime receives a {@code hasBearerTokenProvider}
+ * flag and calls back over the session-scoped {@code providerToken.getToken}
+ * RPC before each model request. Return the raw token without a {@code Bearer }
+ * prefix.
+ *
+ * @param getBearerToken
+ * the bearer-token provider callback
+ * @return this config for method chaining
+ */
+ public ProviderConfig setGetBearerToken(GetBearerToken getBearerToken) {
+ this.getBearerToken = getBearerToken;
+ return this;
+ }
+
+ @JsonProperty("hasBearerTokenProvider")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ Boolean hasBearerTokenProviderWireFlag() {
+ return getBearerToken != null ? Boolean.TRUE : null;
+ }
+
/**
* Gets the Azure-specific options.
*
diff --git a/java/src/main/java/com/github/copilot/rpc/ProviderTokenArgs.java b/java/src/main/java/com/github/copilot/rpc/ProviderTokenArgs.java
new file mode 100644
index 000000000..3866cc0ad
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/rpc/ProviderTokenArgs.java
@@ -0,0 +1,63 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.github.copilot.CopilotExperimental;
+
+/**
+ * Arguments passed to a BYOK bearer-token provider callback.
+ *
+ * Experimental. This managed-identity surface may change or be
+ * removed in future SDK or CLI releases.
+ *
+ * @since 1.0.0
+ */
+@CopilotExperimental
+public class ProviderTokenArgs {
+
+ private String providerName;
+
+ /**
+ * Creates an empty argument object.
+ */
+ public ProviderTokenArgs() {
+ }
+
+ /**
+ * Creates argument object for the named provider.
+ *
+ * @param providerName
+ * the name of the BYOK provider needing a token; {@code "default"}
+ * for the singular whole-session provider, otherwise the named
+ * provider's {@code name}
+ */
+ public ProviderTokenArgs(String providerName) {
+ this.providerName = providerName;
+ }
+
+ /**
+ * Gets the name of the BYOK provider needing a token.
+ *
+ * The value is {@code "default"} for the singular whole-session provider,
+ * otherwise the named provider's {@code name}.
+ *
+ * @return the provider name
+ */
+ public String getProviderName() {
+ return providerName;
+ }
+
+ /**
+ * Sets the name of the BYOK provider needing a token.
+ *
+ * @param providerName
+ * the provider name
+ * @return this args instance for method chaining
+ */
+ public ProviderTokenArgs setProviderName(String providerName) {
+ this.providerName = providerName;
+ return this;
+ }
+}
diff --git a/java/src/test/java/com/github/copilot/ByokBearerTokenProviderE2ETest.java b/java/src/test/java/com/github/copilot/ByokBearerTokenProviderE2ETest.java
new file mode 100644
index 000000000..253ce136c
--- /dev/null
+++ b/java/src/test/java/com/github/copilot/ByokBearerTokenProviderE2ETest.java
@@ -0,0 +1,274 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot;
+
+import static com.github.copilot.CopilotRequestTestSupport.buildNonInferenceResponse;
+import static com.github.copilot.CopilotRequestTestSupport.newLlmClient;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLSession;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.rpc.GetBearerToken;
+import com.github.copilot.rpc.MessageOptions;
+import com.github.copilot.rpc.NamedProviderConfig;
+import com.github.copilot.rpc.PermissionHandler;
+import com.github.copilot.rpc.ProviderModelConfig;
+import com.github.copilot.rpc.SessionConfig;
+
+/**
+ * End-to-end coverage for the experimental BYOK bearer-token-provider surface
+ * ({@code getBearerToken} on a provider config). The callback stays entirely on
+ * the SDK/client side: the SDK keeps it off the wire, sends only the
+ * {@code hasBearerTokenProvider} flag, and the runtime calls back over the
+ * session-scoped {@code providerToken.getToken} RPC before each outbound model
+ * request.
+ */
+public class ByokBearerTokenProviderE2ETest {
+
+ private static final String PRIMARY_HOST = "byok-endpoint.invalid";
+ private static final String PRIMARY_BASE_URL = "https://" + PRIMARY_HOST + "/v1";
+ private static final String RED_HOST = "byok-red.invalid";
+ private static final String RED_BASE_URL = "https://" + RED_HOST + "/v1";
+ private static final String BLUE_HOST = "byok-blue.invalid";
+ private static final String BLUE_BASE_URL = "https://" + BLUE_HOST + "/v1";
+
+ private static E2ETestContext ctx;
+ private CapturingRequestHandler handler;
+
+ @BeforeAll
+ static void setup() throws Exception {
+ ctx = E2ETestContext.create();
+ }
+
+ @AfterAll
+ static void teardown() throws Exception {
+ if (ctx != null) {
+ ctx.close();
+ }
+ }
+
+ @BeforeEach
+ void resetHandler() {
+ handler = new CapturingRequestHandler();
+ }
+
+ @Test
+ void appliesCallbackTokenAsAuthorizationHeader() throws Exception {
+ String sentinel = "sentinel-bearer-token-abc123";
+ AtomicInteger calls = new AtomicInteger();
+ GetBearerToken getBearerToken = args -> {
+ calls.incrementAndGet();
+ return CompletableFuture.completedFuture(sentinel);
+ };
+
+ List providers = List.of(new NamedProviderConfig().setName("mi").setType("openai")
+ .setWireApi("completions").setBaseUrl(PRIMARY_BASE_URL).setGetBearerToken(getBearerToken));
+ List models = List
+ .of(new ProviderModelConfig().setId("default").setProvider("mi").setWireModel("byok-gpt-4o"));
+
+ runTurn(providers, models, "mi/default", "What is 5+5?");
+
+ assertTrue(handler.authHeaders().contains("Bearer " + sentinel),
+ "Expected captured Authorization headers to contain the callback token: " + handler.authHeaders());
+ assertTrue(calls.get() >= 1, "Expected the callback to be invoked at least once");
+ }
+
+ @Test
+ void reacquiresFreshTokenForEachRequest() throws Exception {
+ AtomicInteger calls = new AtomicInteger();
+ GetBearerToken getBearerToken = args -> CompletableFuture
+ .completedFuture("rotating-token-" + calls.incrementAndGet());
+
+ List providers = List.of(new NamedProviderConfig().setName("mi").setType("openai")
+ .setWireApi("completions").setBaseUrl(PRIMARY_BASE_URL).setGetBearerToken(getBearerToken));
+ List models = List
+ .of(new ProviderModelConfig().setId("default").setProvider("mi").setWireModel("byok-gpt-4o"));
+
+ runTurn(providers, models, "mi/default", "What is 1+1?");
+ runTurn(providers, models, "mi/default", "What is 2+2?");
+
+ List auths = handler.authHeaders();
+ assertTrue(auths.size() >= 2, "Expected at least two captured Authorization headers, got " + auths);
+ assertTrue(auths.get(0).startsWith("Bearer rotating-token-"), "Expected rotating token, got " + auths);
+ assertTrue(auths.get(1).startsWith("Bearer rotating-token-"), "Expected rotating token, got " + auths);
+ assertNotEquals(auths.get(0), auths.get(1), "Expected distinct tokens per request");
+ assertTrue(calls.get() >= 2, "Expected the callback to be invoked at least twice");
+ }
+
+ @Test
+ void dispatchesTokenAcquisitionPerProvider() throws Exception {
+ List acquiredFor = new ArrayList<>();
+ GetBearerToken redCallback = args -> {
+ assertEquals("red", args.getProviderName(), "Expected providerName to be forwarded");
+ synchronized (acquiredFor) {
+ acquiredFor.add("red");
+ }
+ return CompletableFuture.completedFuture("token-for-red");
+ };
+ GetBearerToken blueCallback = args -> {
+ assertEquals("blue", args.getProviderName(), "Expected providerName to be forwarded");
+ synchronized (acquiredFor) {
+ acquiredFor.add("blue");
+ }
+ return CompletableFuture.completedFuture("token-for-blue");
+ };
+
+ List providers = List.of(
+ new NamedProviderConfig().setName("red").setType("openai").setWireApi("completions")
+ .setBaseUrl(RED_BASE_URL).setGetBearerToken(redCallback),
+ new NamedProviderConfig().setName("blue").setType("openai").setWireApi("completions")
+ .setBaseUrl(BLUE_BASE_URL).setGetBearerToken(blueCallback));
+ List models = List.of(
+ new ProviderModelConfig().setId("default").setProvider("red").setWireModel("byok-gpt-4o"),
+ new ProviderModelConfig().setId("default").setProvider("blue").setWireModel("byok-gpt-4o"));
+
+ runTurn(providers, models, "red/default", "What is 3+3?");
+ runTurn(providers, models, "blue/default", "What is 4+4?");
+
+ assertEquals("Bearer token-for-red", handler.authHeaderForHost(RED_HOST));
+ assertEquals("Bearer token-for-blue", handler.authHeaderForHost(BLUE_HOST));
+ synchronized (acquiredFor) {
+ assertTrue(acquiredFor.contains("red"), "Expected red provider to acquire a token");
+ assertTrue(acquiredFor.contains("blue"), "Expected blue provider to acquire a token");
+ }
+ }
+
+ private void runTurn(List providers, List models, String selectionId,
+ String prompt) throws Exception {
+ try (CopilotClient client = newLlmClient(ctx, handler)) {
+ CopilotSession session = client
+ .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setModel(selectionId).setProviders(providers).setModels(models))
+ .get(60, TimeUnit.SECONDS);
+ try {
+ session.sendAndWait(new MessageOptions().setPrompt(prompt)).get(60, TimeUnit.SECONDS);
+ } catch (Exception ignored) {
+ // The fake BYOK endpoint returns 404 after capturing the token-bearing request.
+ } finally {
+ try {
+ session.close();
+ } catch (Exception ignored) {
+ // Ignore disconnect errors for the fake BYOK endpoint.
+ }
+ }
+ }
+ }
+
+ private static final class CapturingRequestHandler extends CopilotRequestHandler {
+
+ private final ConcurrentLinkedQueue captures = new ConcurrentLinkedQueue<>();
+
+ @Override
+ protected HttpResponse sendRequest(HttpRequest request, CopilotRequestContext rctx)
+ throws Exception {
+ String host = request.uri().getHost();
+ if (host != null && host.endsWith(".invalid")) {
+ captures.add(new CapturedRequest(request.uri().getHost(),
+ request.headers().firstValue("Authorization").orElse(null)));
+ return new StubHttpResponse(404, "{\"error\":{\"message\":\"fake byok endpoint\"}}");
+ }
+ return buildNonInferenceResponse(request.uri().toString());
+ }
+
+ List authHeaders() {
+ List auths = new ArrayList<>();
+ for (CapturedRequest capture : captures) {
+ if (capture.authorization() != null) {
+ auths.add(capture.authorization());
+ }
+ }
+ return auths;
+ }
+
+ String authHeaderForHost(String host) {
+ for (CapturedRequest capture : captures) {
+ if (host.equals(capture.host())) {
+ return capture.authorization();
+ }
+ }
+ return null;
+ }
+ }
+
+ private static final class StubHttpResponse implements HttpResponse {
+
+ private final int status;
+ private final HttpHeaders headers;
+ private final byte[] body;
+
+ StubHttpResponse(int status, String body) {
+ this.status = status;
+ this.body = body.getBytes(StandardCharsets.UTF_8);
+ this.headers = HttpHeaders.of(Map.of("content-type", List.of("application/json")), (k, v) -> true);
+ }
+
+ @Override
+ public int statusCode() {
+ return status;
+ }
+
+ @Override
+ public HttpRequest request() {
+ return null;
+ }
+
+ @Override
+ public Optional> previousResponse() {
+ return Optional.empty();
+ }
+
+ @Override
+ public HttpHeaders headers() {
+ return headers;
+ }
+
+ @Override
+ public InputStream body() {
+ return new ByteArrayInputStream(body);
+ }
+
+ @Override
+ public Optional sslSession() {
+ return Optional.empty();
+ }
+
+ @Override
+ public URI uri() {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Version version() {
+ return HttpClient.Version.HTTP_1_1;
+ }
+ }
+
+ private record CapturedRequest(String host, String authorization) {
+ }
+}
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 96ac60842..1c29dd53c 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -51,11 +51,14 @@ import type {
ExitPlanModeResult,
ForegroundSessionInfo,
GetAuthStatusResponse,
+ GetBearerToken,
GetStatusResponse,
InternalRuntimeConnection,
LargeToolOutputConfig,
MCPServerConfig,
ModelInfo,
+ NamedProviderConfig,
+ ProviderConfig,
ResumeSessionConfig,
SectionTransformFn,
SessionConfig,
@@ -154,6 +157,62 @@ function toJsonSchema(parameters: Tool["parameters"]): Record |
return parameters;
}
+/** Implicit provider name for the singular, whole-session {@link ProviderConfig}. */
+const DEFAULT_PROVIDER_NAME = "default";
+
+/** Wire-safe singular provider config carrying the `hasBearerTokenProvider` flag. */
+type WireProviderConfig = Omit & { hasBearerTokenProvider?: boolean };
+
+/** Wire-safe named provider config carrying the `hasBearerTokenProvider` flag. */
+type WireNamedProviderConfig = Omit & {
+ hasBearerTokenProvider?: boolean;
+};
+
+/**
+ * Strips the non-serializable {@link GetBearerToken} callbacks from the singular
+ * and named provider configs before they cross the RPC boundary, replacing each
+ * with a `hasBearerTokenProvider: true` wire flag. The callback closes over its
+ * own token scope/audience, so nothing scope-related crosses the wire — the
+ * runtime only forwards the provider name back when it needs a token.
+ * Returns wire-safe provider configs alongside a map of provider name → callback
+ * for session-side registration.
+ */
+function extractBearerTokenProviders(
+ provider: ProviderConfig | undefined,
+ providers: NamedProviderConfig[] | undefined
+): {
+ wireProvider: WireProviderConfig | undefined;
+ wireProviders: WireNamedProviderConfig[] | undefined;
+ callbacks: Map;
+} {
+ const callbacks = new Map();
+
+ let wireProvider: WireProviderConfig | undefined = provider;
+ if (provider?.getBearerToken) {
+ const { getBearerToken, ...rest } = provider;
+ callbacks.set(DEFAULT_PROVIDER_NAME, getBearerToken);
+ wireProvider = {
+ ...rest,
+ hasBearerTokenProvider: true,
+ };
+ }
+
+ let wireProviders: WireNamedProviderConfig[] | undefined = providers;
+ if (providers?.some((p) => p.getBearerToken)) {
+ wireProviders = providers.map((p) => {
+ if (!p.getBearerToken) return p;
+ const { getBearerToken, ...rest } = p;
+ callbacks.set(p.name, getBearerToken);
+ return {
+ ...rest,
+ hasBearerTokenProvider: true,
+ };
+ });
+ }
+
+ return { wireProvider, wireProviders, callbacks };
+}
+
/**
* Convert MCP server configs from public API format (workingDirectory) to
* wire format (cwd) expected by the runtime.
@@ -1237,6 +1296,15 @@ export class CopilotClient {
const useServerGeneratedId = config.cloud != null && callerSessionId == null;
const localSessionId = useServerGeneratedId ? undefined : (callerSessionId ?? randomUUID());
+ // Strip non-serializable getBearerToken callbacks from provider configs,
+ // replacing them with a wire flag; keep the callbacks for session-side
+ // registration so the runtime can call back to acquire tokens.
+ const {
+ wireProvider: bearerWireProvider,
+ wireProviders: bearerWireProviders,
+ callbacks: bearerTokenCallbacks,
+ } = extractBearerTokenProviders(config.provider, config.providers);
+
// Extract transform callbacks from system message config before serialization.
const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
config.systemMessage
@@ -1254,6 +1322,9 @@ export class CopilotClient {
s.registerTools(config.tools);
s.registerCanvases(config.canvases);
s.registerCommands(config.commands);
+ if (bearerTokenCallbacks.size > 0) {
+ s.registerBearerTokenProviders(bearerTokenCallbacks);
+ }
s.registerPermissionHandler(config.onPermissionRequest);
if (config.onUserInputRequest) {
s.registerUserInputHandler(config.onUserInputRequest);
@@ -1325,9 +1396,9 @@ export class CopilotClient {
availableTools: toolFilterOptions.availableTools,
excludedTools: toolFilterOptions.excludedTools,
toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
- provider: config.provider,
+ provider: bearerWireProvider,
capi: config.capi,
- providers: config.providers,
+ providers: bearerWireProviders,
models: config.models,
enableSessionTelemetry: config.enableSessionTelemetry,
modelCapabilities: config.modelCapabilities,
@@ -1446,6 +1517,14 @@ export class CopilotClient {
session.registerTools(config.tools);
session.registerCanvases(config.canvases);
session.registerCommands(config.commands);
+ const {
+ wireProvider: bearerWireProvider,
+ wireProviders: bearerWireProviders,
+ callbacks: bearerTokenCallbacks,
+ } = extractBearerTokenProviders(config.provider, config.providers);
+ if (bearerTokenCallbacks.size > 0) {
+ session.registerBearerTokenProviders(bearerTokenCallbacks);
+ }
session.registerPermissionHandler(config.onPermissionRequest);
if (config.onUserInputRequest) {
session.registerUserInputHandler(config.onUserInputRequest);
@@ -1512,9 +1591,9 @@ export class CopilotClient {
name: cmd.name,
description: cmd.description,
})),
- provider: config.provider,
+ provider: bearerWireProvider,
capi: config.capi,
- providers: config.providers,
+ providers: bearerWireProviders,
models: config.models,
modelCapabilities: config.modelCapabilities,
largeOutput: toWireLargeOutput(config.largeOutput),
diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts
index df44de84c..423ab4fe8 100644
--- a/nodejs/src/generated/rpc.ts
+++ b/nodejs/src/generated/rpc.ts
@@ -6597,6 +6597,10 @@ export interface NamedProviderConfig {
headers?: {
[k: string]: string | undefined;
};
+ /**
+ * When true, the SDK client supplies bearer tokens on demand: the runtime calls the client-session `providerToken.getToken` callback before each request and uses the returned token as the Authorization header. The token-acquiring function itself stays on the SDK side and is never serialized; only this flag crosses the wire. Mutually exclusive with `apiKey`/`bearerToken`.
+ */
+ hasBearerTokenProvider?: boolean;
}
/**
* Azure-specific provider options.
@@ -8575,6 +8579,10 @@ export interface ProviderConfig {
headers?: {
[k: string]: string | undefined;
};
+ /**
+ * When true, the SDK client supplies bearer tokens on demand: the runtime calls the client-session `providerToken.getToken` callback before each request and uses the returned token as the Authorization header. The token-acquiring function itself stays on the SDK side and is never serialized; only this flag crosses the wire. Mutually exclusive with `apiKey`/`bearerToken`.
+ */
+ hasBearerTokenProvider?: boolean;
}
/**
* A snapshot of the provider endpoint the session is currently configured to talk to.
@@ -13627,6 +13635,36 @@ export interface WorkspacesSaveLargePasteResult {
sizeBytes: number;
} | null;
}
+/**
+ * Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "ProviderTokenAcquireRequest".
+ */
+/** @experimental */
+export interface ProviderTokenAcquireRequest {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the BYOK provider needing a token. For the legacy whole-session `provider` this is the implicit provider name; for named providers it is `NamedProviderConfig.name`.
+ */
+ providerName: string;
+}
+/**
+ * A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh.
+ *
+ * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema
+ * via the `definition` "ProviderTokenAcquireResult".
+ */
+/** @experimental */
+export interface ProviderTokenAcquireResult {
+ /**
+ * The bearer token value (without the `Bearer ` prefix).
+ */
+ token: string;
+}
/**
* Standard MCP CallToolResult
*
@@ -15920,10 +15958,24 @@ export interface CanvasHandler {
invoke(params: CanvasProviderInvokeActionRequest): Promise;
}
+/** Handler for `providerToken` client session API methods. */
+/** @experimental */
+export interface ProviderTokenHandler {
+ /**
+ * Asks the SDK client to get a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Session-scoped: the runtime calls it back on the connection that created the session, passing the provider name, and uses the returned token as the Authorization header for the outbound model request. The runtime does no caching — it calls this once per outbound request; the SDK consumer owns token acquisition, caching, and refresh.
+ *
+ * @param params Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request.
+ *
+ * @returns A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh.
+ */
+ getToken(params: ProviderTokenAcquireRequest): Promise;
+}
+
/** All client session API handler groups. */
export interface ClientSessionApiHandlers {
sessionFs?: SessionFsHandler;
canvas?: CanvasHandler;
+ providerToken?: ProviderTokenHandler;
}
/**
@@ -16011,6 +16063,11 @@ export function registerClientSessionApiHandlers(
if (!handler) throw new Error(`No canvas handler registered for session: ${params.sessionId}`);
return handler.invoke(params);
});
+ connection.onRequest("providerToken.getToken", async (params: ProviderTokenAcquireRequest) => {
+ const handler = getHandlers(params.sessionId).providerToken;
+ if (!handler) throw new Error(`No providerToken handler registered for session: ${params.sessionId}`);
+ return handler.getToken(params);
+ });
}
/** Handler for `llmInference` client global API methods. */
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index 9bf02a32c..740a7bc89 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -84,6 +84,7 @@ export type {
MCPHTTPServerConfig,
MCPServerConfig,
DefaultAgentConfig,
+ GetBearerToken,
MessageOptions,
ModelBilling,
ModelBillingTokenPrices,
@@ -99,6 +100,7 @@ export type {
PermissionRequestResult,
ProviderConfig,
ProviderModelConfig,
+ ProviderTokenArgs,
RemoteSessionMode,
ResumeSessionConfig,
SectionOverride,
diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts
index 0ba42ab76..7ffc8d26f 100644
--- a/nodejs/src/session.ts
+++ b/nodejs/src/session.ts
@@ -26,6 +26,7 @@ import type {
ExitPlanModeHandler,
ExitPlanModeRequest,
ExitPlanModeResult,
+ GetBearerToken,
UiInputOptions,
MessageOptions,
PermissionHandler,
@@ -122,6 +123,7 @@ export class CopilotSession {
new Map();
private toolHandlers: Map = new Map();
private canvases: Map = new Map();
+ private bearerTokenProviders: Map = new Map();
private commandHandlers: Map = new Map();
private permissionHandler?: PermissionHandler;
private userInputHandler?: UserInputHandler;
@@ -797,6 +799,45 @@ export class CopilotSession {
};
}
+ /**
+ * Registers per-provider {@link GetBearerToken} callbacks for BYOK providers
+ * configured with managed-identity / on-demand bearer-token auth.
+ *
+ * The runtime never receives the callback itself; the SDK strips it from the
+ * provider config and instead sends `hasBearerTokenProvider: true`. When the
+ * runtime needs a token it issues a session-scoped `providerToken.getToken`
+ * request, which this handler routes to the matching per-provider callback.
+ *
+ * @param providers - Map of provider name → callback, or undefined/empty to clear.
+ * @internal This method is called internally when creating/resuming a session.
+ */
+ registerBearerTokenProviders(providers?: Map): void {
+ this.bearerTokenProviders.clear();
+ if (!providers || providers.size === 0) {
+ delete this.clientSessionApis.providerToken;
+ return;
+ }
+ for (const [name, callback] of providers) {
+ this.bearerTokenProviders.set(name, callback);
+ }
+
+ const self = this;
+ this.clientSessionApis.providerToken = {
+ async getToken(params) {
+ const callback = self.bearerTokenProviders.get(params.providerName);
+ if (!callback) {
+ throw new Error(
+ `No bearer-token provider registered for provider "${params.providerName}"`
+ );
+ }
+ const token = await callback({
+ providerName: params.providerName,
+ });
+ return { token };
+ },
+ };
+ }
+
/**
* Registers command handlers for this session.
*
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index bdf02a7b0..9ed724d55 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -2192,6 +2192,39 @@ export interface ResumeSessionConfig extends SessionConfigBase {
openCanvases?: OpenCanvasInstance[];
}
+/**
+ * Arguments passed to a {@link GetBearerToken} callback when the runtime needs a
+ * fresh bearer token for a BYOK provider.
+ *
+ * @experimental Part of the experimental managed-identity / bearer-token-provider
+ * surface and may change or be removed in future SDK or CLI releases.
+ */
+export interface ProviderTokenArgs {
+ /**
+ * Name of the BYOK provider needing a token. For the singular, whole-session
+ * {@link ProviderConfig} this is the implicit provider name (`"default"`); for
+ * {@link NamedProviderConfig} entries it is {@link NamedProviderConfig.name}.
+ *
+ * The callback closes over its own token scope/audience; the runtime is
+ * provider-agnostic and forwards only the provider name.
+ */
+ providerName: string;
+}
+
+/**
+ * Per-provider callback that resolves a bearer token on demand, returning the
+ * raw token string (without the `Bearer ` prefix). The Copilot SDK itself takes
+ * no Azure dependency: the consumer supplies this callback backed by their own
+ * identity library (for example `@azure/identity`'s
+ * `DefaultAzureCredential.getToken(scope)`), and the runtime calls it once before
+ * each outbound model request. The runtime does no caching of its own, so the
+ * callback (or the identity library it wraps) owns token caching and refresh.
+ *
+ * @experimental Part of the experimental managed-identity / bearer-token-provider
+ * surface and may change or be removed in future SDK or CLI releases.
+ */
+export type GetBearerToken = (args: ProviderTokenArgs) => Promise;
+
/**
* Configuration for a custom API provider.
*/
@@ -2234,6 +2267,18 @@ export interface ProviderConfig {
*/
bearerToken?: string;
+ /**
+ * Per-request bearer-token provider for managed-identity / on-demand auth.
+ * When set, the SDK keeps this function client-side (it is never serialized)
+ * and the runtime calls back into this client to acquire a token before each
+ * outbound request. The runtime does no caching of its own, so the callback
+ * owns token caching and refresh. Mutually exclusive with {@link apiKey} /
+ * {@link bearerToken}.
+ *
+ * @experimental
+ */
+ getBearerToken?: GetBearerToken;
+
/**
* Azure-specific options
*/
@@ -2325,6 +2370,18 @@ export interface NamedProviderConfig {
*/
bearerToken?: string;
+ /**
+ * Per-request bearer-token provider for managed-identity / on-demand auth.
+ * When set, the SDK keeps this function client-side (it is never serialized)
+ * and the runtime calls back into this client to acquire a token before each
+ * outbound request. The runtime does no caching of its own, so the callback
+ * owns token caching and refresh. Mutually exclusive with {@link apiKey} /
+ * {@link bearerToken}.
+ *
+ * @experimental
+ */
+ getBearerToken?: GetBearerToken;
+
/**
* Azure-specific options.
*/
diff --git a/nodejs/test/e2e/byok_bearer_token_provider.e2e.test.ts b/nodejs/test/e2e/byok_bearer_token_provider.e2e.test.ts
new file mode 100644
index 000000000..228b7a022
--- /dev/null
+++ b/nodejs/test/e2e/byok_bearer_token_provider.e2e.test.ts
@@ -0,0 +1,255 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+import { beforeEach, describe, expect, it } from "vitest";
+import { approveAll, CopilotRequestHandler } from "../../src/index.js";
+import type {
+ CopilotRequestContext,
+ GetBearerToken,
+ NamedProviderConfig,
+ ProviderModelConfig,
+} from "../../src/index.js";
+import { createSdkTestContext } from "./harness/sdkTestContext.js";
+
+/**
+ * A captured outbound HTTP request the runtime aimed at a fake BYOK provider
+ * endpoint: just the host and the `Authorization` header, which is all these
+ * tests need to assert on.
+ */
+interface CapturedRequest {
+ host: string;
+ authorization?: string;
+}
+
+// Fake BYOK provider base URLs. These hosts are never actually dialed: the
+// client-global request interceptor fully answers any request aimed at a
+// `.invalid` host, so they only need to be syntactically valid, non-resolving
+// URLs. Distinct hosts let the per-provider test assert routing by host.
+const PRIMARY_HOST = "byok-endpoint.invalid";
+const PRIMARY_BASE_URL = `https://${PRIMARY_HOST}/v1`;
+const RED_HOST = "byok-red.invalid";
+const RED_BASE_URL = `https://${RED_HOST}/v1`;
+const BLUE_HOST = "byok-blue.invalid";
+const BLUE_BASE_URL = `https://${BLUE_HOST}/v1`;
+
+/**
+ * Client-global HTTP request interceptor (from the SDK's `CopilotRequestHandler`
+ * surface) used in place of a real HTTP listener.
+ *
+ * The runtime invokes {@link sendRequest} for every model-layer HTTP request it
+ * would otherwise issue. We capture the ones aimed at a fake BYOK host —
+ * recording the `Authorization` header the runtime applied after calling the
+ * provider's `getBearerToken` callback over the session-scoped
+ * `providerToken.getToken` RPC — and answer them with a synthetic `404` (a
+ * non-retryable status, so each outbound model request yields exactly one
+ * capture). Every other request (CAPI bootstrap: model catalog, policy, …) is
+ * passed straight through to the real network via `super.sendRequest`.
+ *
+ * Because the handler is client-global (one per CLI process), it is installed
+ * once for the whole fixture and {@link reset} between tests.
+ */
+class CapturingRequestHandler extends CopilotRequestHandler {
+ public readonly captures: CapturedRequest[] = [];
+
+ protected override async sendRequest(
+ request: Request,
+ ctx: CopilotRequestContext
+ ): Promise {
+ const url = new URL(request.url);
+ if (url.hostname.endsWith(".invalid")) {
+ this.captures.push({
+ host: url.host,
+ authorization: request.headers.get("authorization") ?? undefined,
+ });
+ return new Response(JSON.stringify({ error: { message: "fake byok endpoint" } }), {
+ status: 404,
+ headers: { "content-type": "application/json" },
+ });
+ }
+ return super.sendRequest(request, ctx);
+ }
+
+ reset(): void {
+ this.captures.length = 0;
+ }
+
+ /** The `Authorization` headers captured across BYOK requests, in arrival order. */
+ authHeaders(): string[] {
+ return this.captures
+ .map((c) => c.authorization)
+ .filter((v): v is string => typeof v === "string");
+ }
+
+ /** The `Authorization` header captured for requests aimed at `host`, if any. */
+ authHeaderForHost(host: string): string | undefined {
+ return this.captures.find((c) => c.host === host)?.authorization;
+ }
+}
+
+/**
+ * End-to-end coverage for the experimental BYOK bearer-token-provider surface
+ * (`getBearerToken` on a provider config). The callback stays entirely on the
+ * SDK/client side: the SDK strips it from the wire config, sets the
+ * `hasBearerTokenProvider` flag, and the runtime calls back over the session-scoped
+ * `providerToken.getToken` RPC before each outbound model request, applying the
+ * returned token as the `Authorization` header.
+ *
+ * Rather than standing up a real HTTP listener, these tests install a
+ * client-global {@link CapturingRequestHandler} that intercepts the runtime's
+ * outbound model request in-process, captures the `Authorization` header, and
+ * returns a synthetic response. They validate, against a real runtime:
+ * 1. the callback's token reaches the model request as `Authorization: Bearer `;
+ * 2. the runtime re-acquires a token per request (no runtime-side caching);
+ * 3. per-provider dispatch routes each provider's turn to its own callback,
+ * and the resulting token reaches that provider's endpoint.
+ */
+describe("BYOK bearer-token provider", async () => {
+ const handler = new CapturingRequestHandler();
+ const { copilotClient: client } = await createSdkTestContext({
+ copilotClientOptions: { requestHandler: handler },
+ });
+
+ beforeEach(() => {
+ handler.reset();
+ });
+
+ /** Drive one BYOK turn; the synthetic 404 errors the turn, which is expected. */
+ async function runTurn(
+ providers: NamedProviderConfig[],
+ models: ProviderModelConfig[],
+ selectionId: string,
+ prompt: string
+ ): Promise {
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ model: selectionId,
+ providers,
+ models,
+ });
+ try {
+ // The interceptor always 404s, so the turn errors after the runtime
+ // has already sent the (token-bearing) request — which is all we
+ // assert on. Swallow the resulting error.
+ await session.sendAndWait({ prompt }).catch(() => undefined);
+ } finally {
+ try {
+ await session.disconnect();
+ } catch {
+ // ignore disconnect errors for the fake BYOK endpoint
+ }
+ }
+ }
+
+ it("applies the callback's token as the Authorization header", async () => {
+ const SENTINEL = "sentinel-bearer-token-abc123";
+ let calls = 0;
+ const getBearerToken: GetBearerToken = async () => {
+ calls += 1;
+ return SENTINEL;
+ };
+
+ const providers: NamedProviderConfig[] = [
+ {
+ name: "mi",
+ type: "openai",
+ wireApi: "completions",
+ baseUrl: PRIMARY_BASE_URL,
+ getBearerToken,
+ },
+ ];
+ const models: ProviderModelConfig[] = [
+ { id: "default", provider: "mi", wireModel: "byok-gpt-4o" },
+ ];
+
+ await runTurn(providers, models, "mi/default", "What is 5+5?");
+
+ // The runtime acquired a token via the callback and applied it verbatim as
+ // the bearer credential on the outbound model request.
+ expect(handler.authHeaders()).toContain(`Bearer ${SENTINEL}`);
+ expect(calls).toBeGreaterThanOrEqual(1);
+ });
+
+ it("re-acquires a fresh token for each request (no runtime caching)", async () => {
+ let calls = 0;
+ const getBearerToken: GetBearerToken = async () => {
+ calls += 1;
+ // A distinct token per acquisition proves the runtime re-invokes the
+ // callback per request rather than caching a previous token.
+ return `rotating-token-${calls}`;
+ };
+
+ const providers: NamedProviderConfig[] = [
+ {
+ name: "mi",
+ type: "openai",
+ wireApi: "completions",
+ baseUrl: PRIMARY_BASE_URL,
+ getBearerToken,
+ },
+ ];
+ const models: ProviderModelConfig[] = [
+ { id: "default", provider: "mi", wireModel: "byok-gpt-4o" },
+ ];
+
+ await runTurn(providers, models, "mi/default", "What is 1+1?");
+ await runTurn(providers, models, "mi/default", "What is 2+2?");
+
+ // Each outbound request carries a freshly-acquired, distinct token.
+ const auths = handler.authHeaders();
+ expect(auths.length).toBeGreaterThanOrEqual(2);
+ expect(auths[0]).toMatch(/^Bearer rotating-token-\d+$/);
+ expect(auths[1]).toMatch(/^Bearer rotating-token-\d+$/);
+ expect(auths[0]).not.toBe(auths[1]);
+ expect(calls).toBeGreaterThanOrEqual(2);
+ });
+
+ it("dispatches token acquisition per provider", async () => {
+ const tokenByProvider: Record = {
+ red: "token-for-red",
+ blue: "token-for-blue",
+ };
+ const acquiredFor: string[] = [];
+ const makeCallback =
+ (providerName: string): GetBearerToken =>
+ async (args) => {
+ // The runtime forwards the requesting provider's name so the client
+ // can dispatch to the right credential.
+ expect(args.providerName).toBe(providerName);
+ acquiredFor.push(providerName);
+ return tokenByProvider[providerName];
+ };
+
+ const providers: NamedProviderConfig[] = [
+ {
+ name: "red",
+ type: "openai",
+ wireApi: "completions",
+ baseUrl: RED_BASE_URL,
+ getBearerToken: makeCallback("red"),
+ },
+ {
+ name: "blue",
+ type: "openai",
+ wireApi: "completions",
+ baseUrl: BLUE_BASE_URL,
+ getBearerToken: makeCallback("blue"),
+ },
+ ];
+ const models: ProviderModelConfig[] = [
+ { id: "default", provider: "red", wireModel: "byok-gpt-4o" },
+ { id: "default", provider: "blue", wireModel: "byok-gpt-4o" },
+ ];
+
+ await runTurn(providers, models, "red/default", "What is 3+3?");
+ await runTurn(providers, models, "blue/default", "What is 4+4?");
+
+ // Each provider's turn was authenticated with its own token AND that token
+ // was delivered to that provider's endpoint, proving per-provider dispatch
+ // (not a single session-global credential).
+ expect(handler.authHeaderForHost(RED_HOST)).toBe(`Bearer ${tokenByProvider.red}`);
+ expect(handler.authHeaderForHost(BLUE_HOST)).toBe(`Bearer ${tokenByProvider.blue}`);
+ expect(acquiredFor).toContain("red");
+ expect(acquiredFor).toContain("blue");
+ });
+});
diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py
index 06ecf4188..1e7a3afb1 100644
--- a/python/copilot/__init__.py
+++ b/python/copilot/__init__.py
@@ -100,6 +100,7 @@
ExitPlanModeHandler,
ExitPlanModeRequest,
ExitPlanModeResult,
+ GetBearerToken,
InfiniteSessionConfig,
InputOptions,
LargeToolOutputConfig,
@@ -128,6 +129,7 @@
PreToolUseHookOutput,
ProviderConfig,
ProviderModelConfig,
+ ProviderTokenArgs,
ReasoningSummary,
SessionCapabilities,
SessionEndHandler,
@@ -214,6 +216,7 @@
"ExtensionInfo",
"CopilotWebSocketForwarder",
"GetAuthStatusResponse",
+ "GetBearerToken",
"GetStatusResponse",
"InfiniteSessionConfig",
"InputOptions",
@@ -257,6 +260,7 @@
"PreToolUseHookOutput",
"ProviderConfig",
"ProviderModelConfig",
+ "ProviderTokenArgs",
"ReasoningSummary",
"RemoteSessionMode",
"RuntimeConnection",
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 5dc670903..cfe031532 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -89,6 +89,7 @@
DefaultAgentConfig,
ElicitationHandler,
ExitPlanModeHandler,
+ GetBearerToken,
InfiniteSessionConfig,
LargeToolOutputConfig,
MCPServerConfig,
@@ -171,6 +172,36 @@ def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]
return wire
+# Implicit provider name for the singular, whole-session ``provider`` config.
+# Named providers are keyed by their own ``name``.
+_DEFAULT_BEARER_TOKEN_PROVIDER_NAME = "default"
+
+
+def _collect_bearer_token_callbacks(
+ provider: ProviderConfig | None,
+ providers: list[NamedProviderConfig] | None,
+) -> dict[str, GetBearerToken]:
+ """Collect per-provider ``get_bearer_token`` callbacks keyed by provider name.
+
+ The singular, whole-session ``provider`` uses the implicit
+ ``_DEFAULT_BEARER_TOKEN_PROVIDER_NAME``; ``providers`` entries use their own
+ ``name``. The callbacks are never serialized — the wire conversion emits
+ ``hasBearerTokenProvider: true`` instead and the runtime calls back over
+ ``providerToken.getToken``.
+ """
+ callbacks: dict[str, GetBearerToken] = {}
+ if provider is not None:
+ singular = provider.get("get_bearer_token")
+ if singular is not None:
+ callbacks[_DEFAULT_BEARER_TOKEN_PROVIDER_NAME] = singular
+ if providers:
+ for named in providers:
+ callback = named.get("get_bearer_token")
+ if callback is not None:
+ callbacks[named["name"]] = callback
+ return callbacks
+
+
def _validate_session_fs_config(config: SessionFsConfig) -> None:
if not config.get("initial_working_directory"):
raise ValueError("session_fs.initial_working_directory is required")
@@ -2112,6 +2143,9 @@ def _initialize_session(sid: str) -> CopilotSession:
s._register_auto_mode_switch_handler(on_auto_mode_switch_request)
if canvas_handler is not None:
s._register_canvas_handler(canvas_handler)
+ s._register_bearer_token_providers(
+ _collect_bearer_token_callbacks(provider, providers)
+ )
if hooks:
s._register_hooks(hooks)
if transform_callbacks:
@@ -2669,6 +2703,9 @@ async def resume_session(
session._register_auto_mode_switch_handler(on_auto_mode_switch_request)
if canvas_handler is not None:
session._register_canvas_handler(canvas_handler)
+ session._register_bearer_token_providers(
+ _collect_bearer_token_callbacks(provider, providers)
+ )
if hooks:
session._register_hooks(hooks)
if transform_callbacks:
@@ -3199,6 +3236,8 @@ def _convert_provider_to_wire_format(
wire_provider["transport"] = provider["transport"]
if "bearer_token" in provider:
wire_provider["bearerToken"] = provider["bearer_token"]
+ if provider.get("get_bearer_token") is not None:
+ wire_provider["hasBearerTokenProvider"] = True
if "headers" in provider:
wire_provider["headers"] = provider["headers"]
if "model_id" in provider:
@@ -3235,6 +3274,8 @@ def _convert_named_provider_to_wire_format(
wire["apiKey"] = provider["api_key"]
if "bearer_token" in provider:
wire["bearerToken"] = provider["bearer_token"]
+ if provider.get("get_bearer_token") is not None:
+ wire["hasBearerTokenProvider"] = True
if "headers" in provider:
wire["headers"] = provider["headers"]
if "azure" in provider:
diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py
index 6b46e465f..f02e59515 100644
--- a/python/copilot/generated/rpc.py
+++ b/python/copilot/generated/rpc.py
@@ -5150,6 +5150,27 @@ def to_dict(self) -> dict:
result["model"] = from_union([from_str, from_none], self.model)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class ProviderTokenAcquireResult:
+ """A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as
+ `Authorization: Bearer ` on the outbound request and does no caching; the SDK
+ consumer owns token caching and refresh.
+ """
+ token: str
+ """The bearer token value (without the `Bearer ` prefix)."""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'ProviderTokenAcquireResult':
+ assert isinstance(obj, dict)
+ token = from_str(obj.get("token"))
+ return ProviderTokenAcquireResult(token)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["token"] = from_str(self.token)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class PushAttachmentFileLineRange:
@@ -11700,6 +11721,13 @@ class NamedProviderConfig:
"""Bearer token for authentication. Sets the Authorization header directly. Takes precedence
over apiKey when both are set.
"""
+ has_bearer_token_provider: bool | None = None
+ """When true, the SDK client supplies bearer tokens on demand: the runtime calls the
+ client-session `providerToken.getToken` callback before each request and uses the
+ returned token as the Authorization header. The token-acquiring function itself stays on
+ the SDK side and is never serialized; only this flag crosses the wire. Mutually exclusive
+ with `apiKey`/`bearerToken`.
+ """
headers: dict[str, str] | None = None
"""Custom HTTP headers to include in all outbound requests to the provider."""
@@ -11717,10 +11745,11 @@ def from_dict(obj: Any) -> 'NamedProviderConfig':
api_key = from_union([from_str, from_none], obj.get("apiKey"))
azure = from_union([ProviderConfigAzure.from_dict, from_none], obj.get("azure"))
bearer_token = from_union([from_str, from_none], obj.get("bearerToken"))
+ has_bearer_token_provider = from_union([from_bool, from_none], obj.get("hasBearerTokenProvider"))
headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("headers"))
type = from_union([ProviderType, from_none], obj.get("type"))
wire_api = from_union([ProviderWireAPI, from_none], obj.get("wireApi"))
- return NamedProviderConfig(base_url, name, api_key, azure, bearer_token, headers, type, wire_api)
+ return NamedProviderConfig(base_url, name, api_key, azure, bearer_token, has_bearer_token_provider, headers, type, wire_api)
def to_dict(self) -> dict:
result: dict = {}
@@ -11732,6 +11761,8 @@ def to_dict(self) -> dict:
result["azure"] = from_union([lambda x: to_class(ProviderConfigAzure, x), from_none], self.azure)
if self.bearer_token is not None:
result["bearerToken"] = from_union([from_str, from_none], self.bearer_token)
+ if self.has_bearer_token_provider is not None:
+ result["hasBearerTokenProvider"] = from_union([from_bool, from_none], self.has_bearer_token_provider)
if self.headers is not None:
result["headers"] = from_union([lambda x: from_dict(from_str, x), from_none], self.headers)
if self.type is not None:
@@ -11758,6 +11789,13 @@ class ProviderConfig:
"""Bearer token for authentication. Sets the Authorization header directly. Takes precedence
over apiKey when both are set.
"""
+ has_bearer_token_provider: bool | None = None
+ """When true, the SDK client supplies bearer tokens on demand: the runtime calls the
+ client-session `providerToken.getToken` callback before each request and uses the
+ returned token as the Authorization header. The token-acquiring function itself stays on
+ the SDK side and is never serialized; only this flag crosses the wire. Mutually exclusive
+ with `apiKey`/`bearerToken`.
+ """
headers: dict[str, str] | None = None
"""Custom HTTP headers to include in all outbound requests to the provider."""
@@ -11792,6 +11830,7 @@ def from_dict(obj: Any) -> 'ProviderConfig':
api_key = from_union([from_str, from_none], obj.get("apiKey"))
azure = from_union([ProviderConfigAzure.from_dict, from_none], obj.get("azure"))
bearer_token = from_union([from_str, from_none], obj.get("bearerToken"))
+ has_bearer_token_provider = from_union([from_bool, from_none], obj.get("hasBearerTokenProvider"))
headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("headers"))
max_context_window_tokens = from_union([from_float, from_none], obj.get("maxContextWindowTokens"))
max_output_tokens = from_union([from_float, from_none], obj.get("maxOutputTokens"))
@@ -11800,7 +11839,7 @@ def from_dict(obj: Any) -> 'ProviderConfig':
type = from_union([ProviderType, from_none], obj.get("type"))
wire_api = from_union([ProviderWireAPI, from_none], obj.get("wireApi"))
wire_model = from_union([from_str, from_none], obj.get("wireModel"))
- return ProviderConfig(base_url, api_key, azure, bearer_token, headers, max_context_window_tokens, max_output_tokens, max_prompt_tokens, model_id, type, wire_api, wire_model)
+ return ProviderConfig(base_url, api_key, azure, bearer_token, has_bearer_token_provider, headers, max_context_window_tokens, max_output_tokens, max_prompt_tokens, model_id, type, wire_api, wire_model)
def to_dict(self) -> dict:
result: dict = {}
@@ -11811,6 +11850,8 @@ def to_dict(self) -> dict:
result["azure"] = from_union([lambda x: to_class(ProviderConfigAzure, x), from_none], self.azure)
if self.bearer_token is not None:
result["bearerToken"] = from_union([from_str, from_none], self.bearer_token)
+ if self.has_bearer_token_provider is not None:
+ result["hasBearerTokenProvider"] = from_union([from_bool, from_none], self.has_bearer_token_provider)
if self.headers is not None:
result["headers"] = from_union([lambda x: from_dict(from_str, x), from_none], self.headers)
if self.max_context_window_tokens is not None:
@@ -16717,6 +16758,33 @@ def to_dict(self) -> dict:
result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesOverrideSupports, x), from_none], self.supports)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class ProviderTokenAcquireRequest:
+ """Asks the SDK client to acquire a bearer token for a BYOK provider whose config set
+ `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request;
+ the runtime does no caching, so this is sent once per request.
+ """
+ provider_name: str
+ """Name of the BYOK provider needing a token. For the legacy whole-session `provider` this
+ is the implicit provider name; for named providers it is `NamedProviderConfig.name`.
+ """
+ session_id: str
+ """Target session identifier"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'ProviderTokenAcquireRequest':
+ assert isinstance(obj, dict)
+ provider_name = from_str(obj.get("providerName"))
+ session_id = from_str(obj.get("sessionId"))
+ return ProviderTokenAcquireRequest(provider_name, session_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["providerName"] = from_str(self.provider_name)
+ result["sessionId"] = from_str(self.session_id)
+ return result
+
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class OptionsUpdateAdditionalContentExclusionPolicy:
@@ -21191,6 +21259,8 @@ class RPC:
provider_get_endpoint_request: ProviderGetEndpointRequest
provider_model_config: ProviderModelConfig
provider_session_token: ProviderSessionToken
+ provider_token_acquire_request: ProviderTokenAcquireRequest
+ provider_token_acquire_result: ProviderTokenAcquireResult
push_attachment: PushAttachment
push_attachment_blob: PushAttachmentBlob
push_attachment_directory: PushAttachmentDirectory
@@ -21957,6 +22027,8 @@ def from_dict(obj: Any) -> 'RPC':
provider_get_endpoint_request = ProviderGetEndpointRequest.from_dict(obj.get("ProviderGetEndpointRequest"))
provider_model_config = ProviderModelConfig.from_dict(obj.get("ProviderModelConfig"))
provider_session_token = ProviderSessionToken.from_dict(obj.get("ProviderSessionToken"))
+ provider_token_acquire_request = ProviderTokenAcquireRequest.from_dict(obj.get("ProviderTokenAcquireRequest"))
+ provider_token_acquire_result = ProviderTokenAcquireResult.from_dict(obj.get("ProviderTokenAcquireResult"))
push_attachment = _load_PushAttachment(obj.get("PushAttachment"))
push_attachment_blob = PushAttachmentBlob.from_dict(obj.get("PushAttachmentBlob"))
push_attachment_directory = PushAttachmentDirectory.from_dict(obj.get("PushAttachmentDirectory"))
@@ -22275,7 +22347,7 @@ def from_dict(obj: Any) -> 'RPC':
subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings"))
task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress"))
workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary"))
- return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agents_get_discovery_paths_request, allow_all_permission_set_result, allow_all_permission_state, api_key_auth_info, auth_info, auth_info_type, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_instance_availability, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_session_context, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, configure_session_extensions_params, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, discovered_canvas, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_filtered_server, mcp_host_state, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_register_external_client_request, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_restart_server_request, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_tools, mcp_unregister_external_client_request, memory_configuration, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_billing, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_allow_all_request, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_allow_all_request, permissions_set_allow_all_source, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, poll_spawned_sessions_result, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, sandbox_config, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_mode, send_request, send_result, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_auth_status, session_bulk_delete_result, session_capability, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_poll_spawned_sessions_event, sessions_poll_spawned_sessions_request, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_cancel_user_requested_request, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, tool, tool_list, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_diff_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, subagent_settings, task_progress, workspace_summary)
+ return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agents_get_discovery_paths_request, allow_all_permission_set_result, allow_all_permission_state, api_key_auth_info, auth_info, auth_info_type, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_instance_availability, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_session_context, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, configure_session_extensions_params, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, discovered_canvas, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_filtered_server, mcp_host_state, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_register_external_client_request, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_restart_server_request, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_tools, mcp_unregister_external_client_request, memory_configuration, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_billing, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_allow_all_request, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_allow_all_request, permissions_set_allow_all_source, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, poll_spawned_sessions_result, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, sandbox_config, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_mode, send_request, send_result, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_auth_status, session_bulk_delete_result, session_capability, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_poll_spawned_sessions_event, sessions_poll_spawned_sessions_request, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_cancel_user_requested_request, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, tool, tool_list, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_diff_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, subagent_settings, task_progress, workspace_summary)
def to_dict(self) -> dict:
result: dict = {}
@@ -22723,6 +22795,8 @@ def to_dict(self) -> dict:
result["ProviderGetEndpointRequest"] = to_class(ProviderGetEndpointRequest, self.provider_get_endpoint_request)
result["ProviderModelConfig"] = to_class(ProviderModelConfig, self.provider_model_config)
result["ProviderSessionToken"] = to_class(ProviderSessionToken, self.provider_session_token)
+ result["ProviderTokenAcquireRequest"] = to_class(ProviderTokenAcquireRequest, self.provider_token_acquire_request)
+ result["ProviderTokenAcquireResult"] = to_class(ProviderTokenAcquireResult, self.provider_token_acquire_result)
result["PushAttachment"] = (self.push_attachment).to_dict()
result["PushAttachmentBlob"] = to_class(PushAttachmentBlob, self.push_attachment_blob)
result["PushAttachmentDirectory"] = to_class(PushAttachmentDirectory, self.push_attachment_directory)
@@ -25096,10 +25170,17 @@ async def invoke(self, params: CanvasProviderInvokeActionRequest) -> Any:
"Invokes an action on an open canvas instance via the provider.\n\nArgs:\n params: Canvas action invocation parameters sent to the provider.\n\nReturns:\n Provider-supplied action result."
pass
+# Experimental: this API group is experimental and may change or be removed.
+class ProviderTokenHandler(Protocol):
+ async def get_token(self, params: ProviderTokenAcquireRequest) -> ProviderTokenAcquireResult:
+ "Asks the SDK client to get a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Session-scoped: the runtime calls it back on the connection that created the session, passing the provider name, and uses the returned token as the Authorization header for the outbound model request. The runtime does no caching — it calls this once per outbound request; the SDK consumer owns token acquisition, caching, and refresh.\n\nArgs:\n params: Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request.\n\nReturns:\n A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh."
+ pass
+
@dataclass
class ClientSessionApiHandlers:
session_fs: SessionFsHandler | None = None
canvas: CanvasHandler | None = None
+ provider_token: ProviderTokenHandler | None = None
def register_client_session_api_handlers(
client: "JsonRpcClient",
@@ -25211,6 +25292,13 @@ async def handle_canvas_action_invoke(params: dict) -> dict | None:
result = await handler.invoke(request)
return result.value if hasattr(result, 'value') else result
client.set_request_handler("canvas.action.invoke", handle_canvas_action_invoke)
+ async def handle_provider_token_get_token(params: dict) -> dict | None:
+ request = ProviderTokenAcquireRequest.from_dict(params)
+ handler = get_handlers(request.session_id).provider_token
+ if handler is None: raise RuntimeError(f"No provider_token handler registered for session: {request.session_id}")
+ result = await handler.get_token(request)
+ return result.to_dict()
+ client.set_request_handler("providerToken.getToken", handle_provider_token_get_token)
# Experimental: this API group is experimental and may change or be removed.
class LlmInferenceHandler(Protocol):
@@ -25776,6 +25864,9 @@ async def handle_llm_inference_http_request_chunk(params: dict) -> dict | None:
"ProviderGetEndpointRequest",
"ProviderModelConfig",
"ProviderSessionToken",
+ "ProviderTokenAcquireRequest",
+ "ProviderTokenAcquireResult",
+ "ProviderTokenHandler",
"ProviderType",
"ProviderWireAPI",
"PurpleSource",
diff --git a/python/copilot/session.py b/python/copilot/session.py
index f15d6c7d3..c1bea85e7 100644
--- a/python/copilot/session.py
+++ b/python/copilot/session.py
@@ -44,6 +44,8 @@
PermissionDecisionApproveOnce,
PermissionDecisionRequest,
PermissionDecisionUserNotAvailable,
+ ProviderTokenAcquireRequest,
+ ProviderTokenAcquireResult,
SessionLogLevel,
SessionRpc,
UIElicitationRequest,
@@ -1066,6 +1068,29 @@ class AzureProviderOptions(TypedDict, total=False):
api_version: str # Azure API version. Defaults to "2024-10-21".
+class ProviderTokenArgs(TypedDict):
+ """Arguments passed to a :data:`GetBearerToken` callback when the runtime
+ needs a fresh bearer token for a BYOK provider.
+
+ **Experimental.** Part of the bearer-token-provider surface and may change or
+ be removed in future SDK or CLI releases.
+ """
+
+ # Name of the BYOK provider needing a token. For the singular, whole-session
+ # ``provider`` this is the implicit provider name ("default"); for
+ # ``NamedProviderConfig`` entries it is ``NamedProviderConfig.name``.
+ provider_name: str
+
+
+# Per-request callback that resolves a bearer token on demand for a BYOK
+# provider (for example via Azure Managed Identity). The Copilot SDK takes no
+# identity dependency: supply a callback backed by your own identity library.
+# Never serialized — setting it makes the SDK send ``hasBearerTokenProvider`` on
+# the wire and answer the runtime's ``providerToken.getToken`` requests. May be
+# sync or async.
+GetBearerToken = Callable[[ProviderTokenArgs], str | Awaitable[str]]
+
+
class ProviderConfig(TypedDict, total=False):
"""Configuration for a custom API provider"""
@@ -1102,6 +1127,12 @@ class ProviderConfig(TypedDict, total=False):
# Overrides the resolved model's default max output tokens. When hit, the
# model stops generating and returns a truncated response.
max_output_tokens: int
+ # Per-request callback that resolves a bearer token on demand for this BYOK
+ # provider (for example via Azure Managed Identity). Never serialized — the
+ # SDK sends hasBearerTokenProvider: true on the wire and answers the
+ # runtime's providerToken.getToken requests with this callback's result.
+ # Mutually exclusive with api_key and bearer_token.
+ get_bearer_token: GetBearerToken
class NamedProviderConfig(TypedDict, total=False):
@@ -1128,6 +1159,11 @@ class NamedProviderConfig(TypedDict, total=False):
bearer_token: str
azure: AzureProviderOptions # Azure-specific options
headers: dict[str, str]
+ # Per-request bearer-token callback for this named BYOK provider. Never
+ # serialized; the SDK sends hasBearerTokenProvider: true and answers the
+ # runtime's providerToken.getToken requests. Mutually exclusive with api_key
+ # and bearer_token.
+ get_bearer_token: GetBearerToken
class ProviderModelConfig(TypedDict, total=False):
@@ -1199,6 +1235,37 @@ def _canvas_handler_error(err: Exception) -> JsonRpcError:
)
+class _BearerTokenProviderAdapter:
+ """Routes runtime ``providerToken.getToken`` requests to the matching
+ per-provider :data:`GetBearerToken` callback registered on the session.
+
+ The runtime calls this once per outbound request for a BYOK provider that
+ declared ``hasBearerTokenProvider: true``; it does no caching, so the SDK
+ consumer's callback (typically backed by an identity library) owns
+ acquisition, caching, and refresh.
+ """
+
+ def __init__(self, session: CopilotSession) -> None:
+ self._session = session
+
+ async def get_token(
+ self, params: ProviderTokenAcquireRequest
+ ) -> ProviderTokenAcquireResult:
+ provider_name = params.provider_name
+ with self._session._bearer_token_providers_lock:
+ callback = self._session._bearer_token_providers.get(provider_name)
+ if callback is None:
+ raise JsonRpcError(
+ -32603,
+ f"No bearer-token provider registered for provider: {provider_name!r}",
+ )
+ args: ProviderTokenArgs = {"provider_name": provider_name}
+ result = callback(args)
+ if inspect.isawaitable(result):
+ result = await result
+ return ProviderTokenAcquireResult(token=cast(str, result))
+
+
class CopilotSession:
"""
Represents a single conversation session with the Copilot CLI.
@@ -1264,6 +1331,8 @@ def __init__(
self._transform_callbacks_lock = threading.Lock()
self._command_handlers: dict[str, CommandHandler] = {}
self._command_handlers_lock = threading.Lock()
+ self._bearer_token_providers: dict[str, GetBearerToken] = {}
+ self._bearer_token_providers_lock = threading.Lock()
self._elicitation_handler: ElicitationHandler | None = None
self._elicitation_handler_lock = threading.Lock()
self._capabilities: SessionCapabilities = {}
@@ -2009,6 +2078,28 @@ def _register_commands(self, commands: list[CommandDefinition] | None) -> None:
for cmd in commands:
self._command_handlers[cmd.name] = cmd.handler
+ def _register_bearer_token_providers(
+ self, providers: dict[str, GetBearerToken] | None
+ ) -> None:
+ """Register per-provider bearer-token callbacks for this session.
+
+ The runtime never receives the callbacks themselves; the SDK strips them
+ from the provider config and instead sends ``hasBearerTokenProvider:
+ true``. When the runtime needs a token it issues a session-scoped
+ ``providerToken.getToken`` request, which the registered handler routes
+ to the matching per-provider callback.
+
+ Args:
+ providers: Map of provider name -> callback, or None/empty to clear.
+ """
+ with self._bearer_token_providers_lock:
+ self._bearer_token_providers.clear()
+ if not providers:
+ self._client_session_apis.provider_token = None
+ return
+ self._bearer_token_providers.update(providers)
+ self._client_session_apis.provider_token = _BearerTokenProviderAdapter(self)
+
def _register_elicitation_handler(self, handler: ElicitationHandler | None) -> None:
"""Register the elicitation handler for this session.
diff --git a/python/e2e/test_byok_bearer_token_provider_e2e.py b/python/e2e/test_byok_bearer_token_provider_e2e.py
new file mode 100644
index 000000000..44e238dd1
--- /dev/null
+++ b/python/e2e/test_byok_bearer_token_provider_e2e.py
@@ -0,0 +1,253 @@
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# --------------------------------------------------------------------------------------------
+
+"""E2E coverage for the experimental BYOK bearer-token-provider surface.
+
+Mirrors ``nodejs/test/e2e/byok_bearer_token_provider.e2e.test.ts``. A BYOK
+provider config may carry a ``get_bearer_token`` callback; the callback stays
+entirely on the SDK/client side. The SDK strips it from the wire config, sets
+the ``hasBearerTokenProvider`` flag, and the runtime calls back over the
+session-scoped ``providerToken.getToken`` RPC before each outbound model
+request, applying the returned token as the ``Authorization`` header.
+
+Like the other ``copilot_request_*`` tests, this one installs a client-global
+``CopilotRequestHandler`` instead of using the CAPI proxy: the handler
+fabricates the bootstrap (catalog/policy) responses and intercepts the
+runtime's outbound BYOK request in-process, capturing the ``Authorization``
+header and returning a synthetic ``404``. It validates, against a real runtime:
+ 1. the callback's token reaches the model request as ``Authorization: Bearer ``;
+ 2. the runtime re-acquires a token per request (no runtime-side caching);
+ 3. per-provider dispatch routes each provider's turn to its own callback, and
+ the resulting token reaches that provider's endpoint.
+"""
+
+from __future__ import annotations
+
+import re
+
+import httpx
+import pytest
+import pytest_asyncio
+
+from copilot import CopilotRequestContext, CopilotRequestHandler
+from copilot.session import GetBearerToken, PermissionHandler
+
+from ._copilot_request_helpers import build_isolated_client, build_non_inference_response
+from .testharness import E2ETestContext
+
+pytestmark = pytest.mark.asyncio(loop_scope="module")
+
+# Fake BYOK provider base URLs. These hosts are never actually dialed: the
+# client-global request interceptor fully answers any request aimed at a
+# ``.invalid`` host, so they only need to be syntactically valid, non-resolving
+# URLs. Distinct hosts let the per-provider test assert routing by host.
+PRIMARY_HOST = "byok-endpoint.invalid"
+PRIMARY_BASE_URL = f"https://{PRIMARY_HOST}/v1"
+RED_HOST = "byok-red.invalid"
+RED_BASE_URL = f"https://{RED_HOST}/v1"
+BLUE_HOST = "byok-blue.invalid"
+BLUE_BASE_URL = f"https://{BLUE_HOST}/v1"
+
+
+class _CapturingRequestHandler(CopilotRequestHandler):
+ """Client-global HTTP interceptor used in place of a real BYOK listener.
+
+ The runtime invokes :meth:`send_request` for every model-layer HTTP request.
+ Requests aimed at a fake BYOK host are captured — recording the
+ ``Authorization`` header the runtime applied after calling the provider's
+ ``get_bearer_token`` callback over ``providerToken.getToken`` — and answered
+ with a synthetic ``404`` (non-retryable, so each outbound model request
+ yields exactly one capture). Every other request (CAPI bootstrap: model
+ catalog, policy, …) is fabricated locally so no real network or CAPI proxy
+ is involved.
+ """
+
+ def __init__(self) -> None:
+ # (host, authorization) for each captured BYOK request, in arrival order.
+ self.captures: list[tuple[str, str | None]] = []
+
+ async def send_request(
+ self, request: httpx.Request, ctx: CopilotRequestContext
+ ) -> httpx.Response:
+ url = httpx.URL(request.url)
+ host = url.host
+ if host.endswith(".invalid"):
+ self.captures.append((host, request.headers.get("authorization")))
+ return httpx.Response(
+ 404,
+ headers={"content-type": "application/json"},
+ json={"error": {"message": "fake byok endpoint"}},
+ request=request,
+ )
+ return build_non_inference_response(str(request.url))
+
+ def reset(self) -> None:
+ self.captures.clear()
+
+ def auth_headers(self) -> list[str]:
+ """The ``Authorization`` headers captured across BYOK requests, in order."""
+ return [auth for (_host, auth) in self.captures if auth is not None]
+
+ def auth_header_for_host(self, host: str) -> str | None:
+ """The ``Authorization`` header captured for requests aimed at ``host``."""
+ for captured_host, auth in self.captures:
+ if captured_host == host:
+ return auth
+ return None
+
+
+@pytest_asyncio.fixture(loop_scope="module")
+async def bearer_fixture(ctx: E2ETestContext):
+ handler = _CapturingRequestHandler()
+ client = build_isolated_client(ctx, handler)
+ await client.start()
+ try:
+ yield client, handler
+ finally:
+ try:
+ await client.stop()
+ except Exception:
+ # Best-effort teardown during fixture cleanup.
+ pass
+
+
+async def _run_turn(client, providers, models, selection_id: str, prompt: str) -> None:
+ """Drive one BYOK turn; the synthetic 404 errors the turn, which is expected."""
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model=selection_id,
+ providers=providers,
+ models=models,
+ )
+ try:
+ # The interceptor always 404s, so the turn errors after the runtime has
+ # already sent the (token-bearing) request — which is all we assert on.
+ try:
+ await session.send_and_wait(prompt)
+ except Exception:
+ pass
+ finally:
+ try:
+ await session.disconnect()
+ except Exception:
+ # ignore disconnect errors for the fake BYOK endpoint
+ pass
+
+
+class TestByokBearerTokenProvider:
+ async def test_applies_the_callbacks_token_as_the_authorization_header(
+ self, bearer_fixture
+ ):
+ client, handler = bearer_fixture
+ handler.reset()
+
+ sentinel = "sentinel-bearer-token-abc123"
+ calls = 0
+
+ async def get_bearer_token(args) -> str:
+ nonlocal calls
+ calls += 1
+ return sentinel
+
+ providers = [
+ {
+ "name": "mi",
+ "type": "openai",
+ "wire_api": "completions",
+ "base_url": PRIMARY_BASE_URL,
+ "get_bearer_token": get_bearer_token,
+ }
+ ]
+ models = [{"id": "default", "provider": "mi", "wire_model": "byok-gpt-4o"}]
+
+ await _run_turn(client, providers, models, "mi/default", "What is 5+5?")
+
+ # The runtime acquired a token via the callback and applied it verbatim
+ # as the bearer credential on the outbound model request.
+ assert f"Bearer {sentinel}" in handler.auth_headers()
+ assert calls >= 1
+
+ async def test_reacquires_a_fresh_token_for_each_request(self, bearer_fixture):
+ client, handler = bearer_fixture
+ handler.reset()
+
+ calls = 0
+
+ async def get_bearer_token(args) -> str:
+ nonlocal calls
+ calls += 1
+ # A distinct token per acquisition proves the runtime re-invokes the
+ # callback per request rather than caching a previous token.
+ return f"rotating-token-{calls}"
+
+ providers = [
+ {
+ "name": "mi",
+ "type": "openai",
+ "wire_api": "completions",
+ "base_url": PRIMARY_BASE_URL,
+ "get_bearer_token": get_bearer_token,
+ }
+ ]
+ models = [{"id": "default", "provider": "mi", "wire_model": "byok-gpt-4o"}]
+
+ await _run_turn(client, providers, models, "mi/default", "What is 1+1?")
+ await _run_turn(client, providers, models, "mi/default", "What is 2+2?")
+
+ # Each outbound request carries a freshly-acquired, distinct token.
+ auths = handler.auth_headers()
+ assert len(auths) >= 2
+ assert re.match(r"^Bearer rotating-token-\d+$", auths[0])
+ assert re.match(r"^Bearer rotating-token-\d+$", auths[1])
+ assert auths[0] != auths[1]
+ assert calls >= 2
+
+ async def test_dispatches_token_acquisition_per_provider(self, bearer_fixture):
+ client, handler = bearer_fixture
+ handler.reset()
+
+ token_by_provider = {"red": "token-for-red", "blue": "token-for-blue"}
+ acquired_for: list[str] = []
+
+ def make_callback(provider_name: str) -> GetBearerToken:
+ async def callback(args) -> str:
+ # The runtime forwards the requesting provider's name so the
+ # client can dispatch to the right credential.
+ assert args["provider_name"] == provider_name
+ acquired_for.append(provider_name)
+ return token_by_provider[provider_name]
+
+ return callback
+
+ providers = [
+ {
+ "name": "red",
+ "type": "openai",
+ "wire_api": "completions",
+ "base_url": RED_BASE_URL,
+ "get_bearer_token": make_callback("red"),
+ },
+ {
+ "name": "blue",
+ "type": "openai",
+ "wire_api": "completions",
+ "base_url": BLUE_BASE_URL,
+ "get_bearer_token": make_callback("blue"),
+ },
+ ]
+ models = [
+ {"id": "default", "provider": "red", "wire_model": "byok-gpt-4o"},
+ {"id": "default", "provider": "blue", "wire_model": "byok-gpt-4o"},
+ ]
+
+ await _run_turn(client, providers, models, "red/default", "What is 3+3?")
+ await _run_turn(client, providers, models, "blue/default", "What is 4+4?")
+
+ # Each provider's turn was authenticated with its own token AND that
+ # token was delivered to that provider's endpoint, proving per-provider
+ # dispatch (not a single session-global credential).
+ assert handler.auth_header_for_host(RED_HOST) == f"Bearer {token_by_provider['red']}"
+ assert handler.auth_header_for_host(BLUE_HOST) == f"Bearer {token_by_provider['blue']}"
+ assert "red" in acquired_for
+ assert "blue" in acquired_for
diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs
index 522222b32..05e73dcd4 100644
--- a/rust/src/generated/api_types.rs
+++ b/rust/src/generated/api_types.rs
@@ -545,6 +545,8 @@ pub mod rpc_methods {
pub const CANVAS_CLOSE: &str = "canvas.close";
/// `canvas.action.invoke`
pub const CANVAS_ACTION_INVOKE: &str = "canvas.action.invoke";
+ /// `providerToken.getToken`
+ pub const PROVIDERTOKEN_GETTOKEN: &str = "providerToken.getToken";
}
/// Parameters for aborting the current turn
@@ -5551,6 +5553,9 @@ pub struct NamedProviderConfig {
/// Bearer token for authentication. Sets the Authorization header directly. Takes precedence over apiKey when both are set.
#[serde(skip_serializing_if = "Option::is_none")]
pub bearer_token: Option,
+ /// When true, the SDK client supplies bearer tokens on demand: the runtime calls the client-session `providerToken.getToken` callback before each request and uses the returned token as the Authorization header. The token-acquiring function itself stays on the SDK side and is never serialized; only this flag crosses the wire. Mutually exclusive with `apiKey`/`bearerToken`.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub has_bearer_token_provider: Option,
/// Custom HTTP headers to include in all outbound requests to the provider.
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option>,
@@ -7682,6 +7687,9 @@ pub struct ProviderConfig {
/// Bearer token for authentication. Sets the Authorization header directly. Takes precedence over apiKey when both are set.
#[serde(skip_serializing_if = "Option::is_none")]
pub bearer_token: Option,
+ /// When true, the SDK client supplies bearer tokens on demand: the runtime calls the client-session `providerToken.getToken` callback before each request and uses the returned token as the Authorization header. The token-acquiring function itself stays on the SDK side and is never serialized; only this flag crosses the wire. Mutually exclusive with `apiKey`/`bearerToken`.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub has_bearer_token_provider: Option,
/// Custom HTTP headers to include in all outbound requests to the provider.
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option>,
@@ -13100,6 +13108,38 @@ pub struct WorkspaceSummary {
pub user_named: Option,
}
+/// Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProviderTokenAcquireRequest {
+ /// Target session identifier
+ pub session_id: SessionId,
+ /// Name of the BYOK provider needing a token. For the legacy whole-session `provider` this is the implicit provider name; for named providers it is `NamedProviderConfig.name`.
+ pub provider_name: String,
+}
+
+/// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProviderTokenAcquireResult {
+ /// The bearer token value (without the `Bearer ` prefix).
+ pub token: String,
+}
+
/// List of Copilot models available to the resolved user, including capabilities and billing metadata.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -16762,6 +16802,21 @@ pub struct CanvasOpenResult {
pub url: Option,
}
+/// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProviderTokenGetTokenResult {
+ /// The bearer token value (without the `Bearer ` prefix).
+ pub token: String,
+}
+
/// HTTP headers as a map from lowercased header name to a list of values. Multi-valued headers (e.g. Set-Cookie) preserve all values.
///
///
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index a0986182f..bd4988e86 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -22,6 +22,9 @@ pub mod hooks;
mod jsonrpc;
/// Permission-policy helpers that produce a [`handler::PermissionHandler`].
pub mod permission;
+/// BYOK bearer-token provider callbacks.
+pub mod provider_token;
+mod provider_token_dispatch;
/// GitHub Copilot CLI binary resolution (env var, embedded, dev cache).
pub(crate) mod resolve;
mod router;
@@ -72,6 +75,7 @@ pub(crate) use jsonrpc::{
JsonRpcClient, JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, error_codes,
};
pub use mode::{BUILTIN_TOOLS_ISOLATED, ClientMode, ToolSet};
+pub use provider_token::{BearerTokenError, BearerTokenProvider, ProviderTokenArgs};
/// Re-exported JSON-RPC internals for integration tests (requires `test-support` feature).
#[cfg(feature = "test-support")]
diff --git a/rust/src/provider_token.rs b/rust/src/provider_token.rs
new file mode 100644
index 000000000..f92715006
--- /dev/null
+++ b/rust/src/provider_token.rs
@@ -0,0 +1,105 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+//! BYOK bearer-token provider callbacks.
+//!
+//!
+//!
+//! **Experimental.** These types are part of an experimental wire-protocol
+//! surface and may change or be removed in future SDK or CLI releases.
+//!
+//!
+
+use std::future::Future;
+
+use async_trait::async_trait;
+
+/// Arguments passed to a BYOK bearer-token provider callback.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol
+/// surface and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProviderTokenArgs {
+ /// Name of the BYOK provider needing a token.
+ ///
+ /// This is `"default"` for the singular whole-session provider, otherwise
+ /// the named provider's `name`.
+ pub provider_name: String,
+}
+
+/// Error returned by a [`BearerTokenProvider`].
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol
+/// surface and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BearerTokenError {
+ message: String,
+}
+
+impl BearerTokenError {
+ /// Construct a bearer-token error with a human-readable message.
+ pub fn message(message: impl Into
) -> Self {
+ Self {
+ message: message.into(),
+ }
+ }
+
+ /// Return the human-readable error message.
+ pub fn as_str(&self) -> &str {
+ &self.message
+ }
+}
+
+impl std::fmt::Display for BearerTokenError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&self.message)
+ }
+}
+
+impl std::error::Error for BearerTokenError {}
+
+impl From for BearerTokenError {
+ fn from(message: String) -> Self {
+ Self::message(message)
+ }
+}
+
+impl From<&str> for BearerTokenError {
+ fn from(message: &str) -> Self {
+ Self::message(message)
+ }
+}
+
+/// Provider-side callback used to acquire bearer tokens for BYOK providers.
+///
+///
+///
+/// **Experimental.** This trait is part of an experimental wire-protocol
+/// surface and may change or be removed in future SDK or CLI releases.
+///
+///
+#[async_trait]
+pub trait BearerTokenProvider: Send + Sync {
+ /// Acquire a bearer token without the `Bearer ` prefix.
+ async fn get_token(&self, args: ProviderTokenArgs) -> Result;
+}
+
+#[async_trait]
+impl BearerTokenProvider for F
+where
+ F: Fn(ProviderTokenArgs) -> Fut + Send + Sync,
+ Fut: Future