diff --git a/dotnet/test/E2E/ProviderEndpointE2ETests.cs b/dotnet/test/E2E/ProviderEndpointE2ETests.cs
new file mode 100644
index 000000000..f7e9a7885
--- /dev/null
+++ b/dotnet/test/E2E/ProviderEndpointE2ETests.cs
@@ -0,0 +1,119 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using GitHub.Copilot.Rpc;
+using GitHub.Copilot.Test.Harness;
+using System.Text.RegularExpressions;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace GitHub.Copilot.Test.E2E;
+
+public class ProviderEndpointE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
+ : E2ETestBase(fixture, "provider-endpoint", output)
+{
+ ///
+ /// Creates a client with the provider-endpoint API opt-in env var
+ /// (COPILOT_ALLOW_GET_PROVIDER_ENDPOINT) set on the CLI subprocess.
+ ///
+ private CopilotClient CreateProviderEndpointClient()
+ {
+ var env = new Dictionary(Ctx.GetEnvironment())
+ {
+ ["COPILOT_ALLOW_GET_PROVIDER_ENDPOINT"] = "true",
+ };
+ return Ctx.CreateClient(options: new CopilotClientOptions { Environment = env });
+ }
+
+ [Fact]
+ public async Task ShouldReturnByokProviderEndpointWhenCustomProviderIsConfigured()
+ {
+ var client = CreateProviderEndpointClient();
+
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ Provider = new ProviderConfig
+ {
+ Type = "openai",
+ WireApi = "completions",
+ BaseUrl = "https://api.example.test/v1",
+ ApiKey = "byok-secret",
+ Headers = new Dictionary { ["X-Custom-Header"] = "byok-yes" },
+ },
+ });
+
+ try
+ {
+ var endpoint = await session.Rpc.Provider.GetEndpointAsync();
+
+ Assert.Equal(ProviderEndpointType.Openai, endpoint.Type);
+ Assert.Equal(ProviderEndpointWireApi.Completions, endpoint.WireApi);
+ Assert.Equal("https://api.example.test/v1", endpoint.BaseUrl);
+ Assert.Equal("byok-secret", endpoint.ApiKey);
+ Assert.Equal("byok-yes", endpoint.Headers["X-Custom-Header"]);
+ // BYOK sessions never issue a CAPI session token.
+ Assert.Null(endpoint.SessionToken);
+ }
+ finally
+ {
+ try { await session.DisposeAsync(); }
+ catch { /* disconnect may fail since the BYOK provider URL is fake */ }
+ }
+ }
+
+ [Fact]
+ public async Task ShouldReturnCapiProviderEndpointForOAuthAuthenticatedSession()
+ {
+ var client = CreateProviderEndpointClient();
+
+ await using var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ });
+
+ var endpoint = await session.Rpc.Provider.GetEndpointAsync();
+
+ Assert.True(
+ endpoint.Type == ProviderEndpointType.Openai
+ || endpoint.Type == ProviderEndpointType.Azure
+ || endpoint.Type == ProviderEndpointType.Anthropic,
+ $"unexpected endpoint.Type {endpoint.Type}");
+ // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes.
+ if (endpoint.Type != ProviderEndpointType.Anthropic)
+ {
+ Assert.True(
+ endpoint.WireApi == ProviderEndpointWireApi.Completions
+ || endpoint.WireApi == ProviderEndpointWireApi.Responses,
+ $"unexpected endpoint.WireApi {endpoint.WireApi}");
+ }
+
+ // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness.
+ Assert.Matches(@"^https?://", endpoint.BaseUrl);
+
+ // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer.
+ Assert.False(string.IsNullOrEmpty(endpoint.ApiKey));
+
+ // Standard CAPI headers should be present, and Authorization is
+ // surfaced as the runtime sends it (`Bearer `).
+ Assert.False(string.IsNullOrEmpty(endpoint.Headers["Copilot-Integration-Id"]));
+ Assert.Matches(new Regex("Copilot", RegexOptions.IgnoreCase), endpoint.Headers["User-Agent"]);
+ Assert.False(string.IsNullOrEmpty(endpoint.Headers["X-GitHub-Api-Version"]));
+ Assert.Matches(@"[0-9a-f-]{8,}", endpoint.Headers["X-Interaction-Id"]);
+ Assert.Equal($"Bearer {endpoint.ApiKey}", endpoint.Headers["Authorization"]);
+
+ // When the omit-modelId path returned an auto-mode session token, it
+ // must use the documented header name. The harness may have a non-auto
+ // model selected, in which case the field is simply omitted.
+ if (endpoint.SessionToken != null)
+ {
+ Assert.Equal("Copilot-Session-Token", endpoint.SessionToken.Header);
+ Assert.False(string.IsNullOrEmpty(endpoint.SessionToken.Token));
+ if (endpoint.SessionToken.ExpiresAt.HasValue)
+ {
+ Assert.True(endpoint.SessionToken.ExpiresAt.Value > DateTimeOffset.MinValue);
+ }
+ }
+ }
+}
diff --git a/go/internal/e2e/provider_endpoint_e2e_test.go b/go/internal/e2e/provider_endpoint_e2e_test.go
new file mode 100644
index 000000000..aad02ca2b
--- /dev/null
+++ b/go/internal/e2e/provider_endpoint_e2e_test.go
@@ -0,0 +1,147 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package e2e
+
+import (
+ "regexp"
+ "strings"
+ "testing"
+
+ copilot "github.com/github/copilot-sdk/go"
+ "github.com/github/copilot-sdk/go/internal/e2e/testharness"
+ "github.com/github/copilot-sdk/go/rpc"
+)
+
+// session.provider.getEndpoint is gated behind COPILOT_ALLOW_GET_PROVIDER_ENDPOINT;
+// the harness env passed to the CLI subprocess opts in for this test file.
+func TestProviderEndpointE2E(t *testing.T) {
+ ctx := testharness.NewTestContext(t)
+
+ client := ctx.NewClient(func(opts *copilot.ClientOptions) {
+ opts.Env = append(opts.Env, "COPILOT_ALLOW_GET_PROVIDER_ENDPOINT=true")
+ })
+ t.Cleanup(func() { client.ForceStop() })
+
+ t.Run("returns the BYOK provider endpoint when a custom provider is configured", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ Provider: &copilot.ProviderConfig{
+ Type: "openai",
+ WireAPI: "completions",
+ BaseURL: "https://api.example.test/v1",
+ APIKey: "byok-secret",
+ Headers: map[string]string{"X-Custom-Header": "byok-yes"},
+ },
+ })
+ if err != nil {
+ t.Fatalf("create session: %v", err)
+ }
+ // disconnect may fail since the BYOK provider URL is fake.
+ defer func() { _ = session.Disconnect() }()
+
+ endpoint, err := session.RPC.Provider.GetEndpoint(t.Context())
+ if err != nil {
+ t.Fatalf("getEndpoint: %v", err)
+ }
+
+ if endpoint.Type != rpc.ProviderEndpointTypeOpenai {
+ t.Errorf("Type: want %q, got %q", rpc.ProviderEndpointTypeOpenai, endpoint.Type)
+ }
+ if endpoint.WireAPI == nil || *endpoint.WireAPI != rpc.ProviderEndpointWireAPICompletions {
+ t.Errorf("WireAPI: want %q, got %v", rpc.ProviderEndpointWireAPICompletions, endpoint.WireAPI)
+ }
+ if endpoint.BaseURL != "https://api.example.test/v1" {
+ t.Errorf("BaseURL: got %q", endpoint.BaseURL)
+ }
+ if endpoint.APIKey == nil || *endpoint.APIKey != "byok-secret" {
+ t.Errorf("APIKey: got %v", endpoint.APIKey)
+ }
+ if got := endpoint.Headers["X-Custom-Header"]; got != "byok-yes" {
+ t.Errorf("X-Custom-Header: got %q", got)
+ }
+ // BYOK sessions never issue a CAPI session token.
+ if endpoint.SessionToken != nil {
+ t.Errorf("SessionToken: expected nil, got %+v", endpoint.SessionToken)
+ }
+ })
+
+ t.Run("returns the CAPI provider endpoint for an OAuth-authenticated session", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ })
+ if err != nil {
+ t.Fatalf("create session: %v", err)
+ }
+ defer func() {
+ if err := session.Disconnect(); err != nil {
+ t.Errorf("disconnect: %v", err)
+ }
+ }()
+
+ endpoint, err := session.RPC.Provider.GetEndpoint(t.Context())
+ if err != nil {
+ t.Fatalf("getEndpoint: %v", err)
+ }
+
+ switch endpoint.Type {
+ case rpc.ProviderEndpointTypeOpenai, rpc.ProviderEndpointTypeAzure, rpc.ProviderEndpointTypeAnthropic:
+ default:
+ t.Errorf("unexpected Type %q", endpoint.Type)
+ }
+ // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes.
+ if endpoint.Type != rpc.ProviderEndpointTypeAnthropic {
+ if endpoint.WireAPI == nil ||
+ (*endpoint.WireAPI != rpc.ProviderEndpointWireAPICompletions &&
+ *endpoint.WireAPI != rpc.ProviderEndpointWireAPIResponses) {
+ t.Errorf("unexpected WireAPI %v for type %q", endpoint.WireAPI, endpoint.Type)
+ }
+ }
+
+ // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness.
+ if !strings.HasPrefix(endpoint.BaseURL, "http://") && !strings.HasPrefix(endpoint.BaseURL, "https://") {
+ t.Errorf("BaseURL not an http(s) URL: %q", endpoint.BaseURL)
+ }
+
+ // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer.
+ if endpoint.APIKey == nil || len(*endpoint.APIKey) == 0 {
+ t.Fatalf("APIKey should be a non-empty string, got %v", endpoint.APIKey)
+ }
+
+ // Standard CAPI headers must be present, and Authorization is surfaced
+ // as the runtime sends it (`Bearer `).
+ if endpoint.Headers["Copilot-Integration-Id"] == "" {
+ t.Errorf("Copilot-Integration-Id header missing")
+ }
+ if ua := endpoint.Headers["User-Agent"]; !regexp.MustCompile(`(?i)Copilot`).MatchString(ua) {
+ t.Errorf("User-Agent should mention Copilot, got %q", ua)
+ }
+ if endpoint.Headers["X-GitHub-Api-Version"] == "" {
+ t.Errorf("X-GitHub-Api-Version header missing")
+ }
+ if !regexp.MustCompile(`[0-9a-f-]{8,}`).MatchString(endpoint.Headers["X-Interaction-Id"]) {
+ t.Errorf("X-Interaction-Id should match interaction-id format, got %q", endpoint.Headers["X-Interaction-Id"])
+ }
+ if want, got := "Bearer "+*endpoint.APIKey, endpoint.Headers["Authorization"]; want != got {
+ t.Errorf("Authorization: want %q, got %q", want, got)
+ }
+
+ // When the omit-modelId path returned an auto-mode session token, it
+ // must use the documented header name. The harness may have a non-auto
+ // model selected, in which case the field is simply omitted.
+ if endpoint.SessionToken != nil {
+ if endpoint.SessionToken.Header != "Copilot-Session-Token" {
+ t.Errorf("SessionToken.Header: got %q", endpoint.SessionToken.Header)
+ }
+ if endpoint.SessionToken.Token == "" {
+ t.Errorf("SessionToken.Token should be non-empty")
+ }
+ if endpoint.SessionToken.ExpiresAt != nil && endpoint.SessionToken.ExpiresAt.IsZero() {
+ t.Errorf("SessionToken.ExpiresAt should be a valid time when present")
+ }
+ }
+ })
+}
diff --git a/java/src/test/java/com/github/copilot/ProviderEndpointE2ETest.java b/java/src/test/java/com/github/copilot/ProviderEndpointE2ETest.java
new file mode 100644
index 000000000..1e302982e
--- /dev/null
+++ b/java/src/test/java/com/github/copilot/ProviderEndpointE2ETest.java
@@ -0,0 +1,155 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.generated.rpc.ProviderEndpointType;
+import com.github.copilot.generated.rpc.ProviderEndpointWireApi;
+import com.github.copilot.generated.rpc.ProviderSessionToken;
+import com.github.copilot.generated.rpc.SessionProviderGetEndpointResult;
+import com.github.copilot.rpc.CopilotClientOptions;
+import com.github.copilot.rpc.PermissionHandler;
+import com.github.copilot.rpc.ProviderConfig;
+import com.github.copilot.rpc.SessionConfig;
+
+/**
+ * Tests for the {@code session.provider.getEndpoint} RPC, which surfaces the
+ * resolved provider endpoint and credentials for either a BYOK or CAPI session.
+ */
+public class ProviderEndpointE2ETest {
+
+ private static E2ETestContext ctx;
+
+ @BeforeAll
+ static void setup() throws Exception {
+ ctx = E2ETestContext.create();
+ }
+
+ @AfterAll
+ static void teardown() throws Exception {
+ if (ctx != null) {
+ ctx.close();
+ }
+ }
+
+ // session.provider.getEndpoint is gated behind
+ // COPILOT_ALLOW_GET_PROVIDER_ENDPOINT;
+ // the harness env passed to the CLI subprocess opts in for these tests.
+ private CopilotClient createProviderEndpointClient() {
+ Map env = new HashMap<>(ctx.getEnvironment());
+ env.put("COPILOT_ALLOW_GET_PROVIDER_ENDPOINT", "true");
+ return ctx.createClient(new CopilotClientOptions().setEnvironment(env));
+ }
+
+ @Test
+ void shouldReturnByokProviderEndpointWhenCustomProviderConfigured() throws Exception {
+ try (CopilotClient client = createProviderEndpointClient()) {
+ Map customHeaders = new HashMap<>();
+ customHeaders.put("X-Custom-Header", "byok-yes");
+
+ ProviderConfig provider = new ProviderConfig().setType("openai").setWireApi("completions")
+ .setBaseUrl("https://api.example.test/v1").setApiKey("byok-secret").setHeaders(customHeaders);
+
+ CopilotSession session = client.createSession(
+ new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setProvider(provider))
+ .get();
+
+ try {
+ SessionProviderGetEndpointResult endpoint = session.getRpc().provider.getEndpoint().get();
+
+ assertEquals(ProviderEndpointType.OPENAI, endpoint.type());
+ assertEquals(ProviderEndpointWireApi.COMPLETIONS, endpoint.wireApi());
+ assertEquals("https://api.example.test/v1", endpoint.baseUrl());
+ assertEquals("byok-secret", endpoint.apiKey());
+ assertEquals("byok-yes", endpoint.headers().get("X-Custom-Header"));
+ // BYOK sessions never issue a CAPI session token.
+ assertNull(endpoint.sessionToken(), "BYOK session should not have a session token");
+ } finally {
+ try {
+ session.close();
+ } catch (Exception ignored) {
+ // disconnect may fail since the BYOK provider URL is fake
+ }
+ }
+ }
+ }
+
+ @Test
+ void shouldReturnCapiProviderEndpointForOAuthAuthenticatedSession() throws Exception {
+ ctx.initializeProxy();
+ ctx.setCopilotUserByToken("fake-token-for-e2e-tests", "e2e-user", "individual_pro", ctx.getProxyUrl(),
+ "https://localhost:1/telemetry", "e2e-tracking-id");
+
+ try (CopilotClient client = createProviderEndpointClient()) {
+ CopilotSession session = client
+ .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
+
+ try {
+ SessionProviderGetEndpointResult endpoint = session.getRpc().provider.getEndpoint().get();
+
+ assertNotNull(endpoint.type(), "CAPI endpoint should have a provider type");
+ assertTrue(
+ endpoint.type() == ProviderEndpointType.OPENAI || endpoint.type() == ProviderEndpointType.AZURE
+ || endpoint.type() == ProviderEndpointType.ANTHROPIC,
+ "expected type in {openai, azure, anthropic}, got " + endpoint.type());
+ // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes.
+ if (endpoint.type() != ProviderEndpointType.ANTHROPIC) {
+ assertTrue(
+ endpoint.wireApi() == ProviderEndpointWireApi.COMPLETIONS
+ || endpoint.wireApi() == ProviderEndpointWireApi.RESPONSES,
+ "expected wireApi in {completions, responses}, got " + endpoint.wireApi());
+ }
+
+ // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness.
+ assertTrue(endpoint.baseUrl().startsWith("http://") || endpoint.baseUrl().startsWith("https://"),
+ "expected http(s) baseUrl, got " + endpoint.baseUrl());
+
+ // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer.
+ assertNotNull(endpoint.apiKey(), "CAPI OAuth session must surface apiKey");
+ assertFalse(endpoint.apiKey().isEmpty(), "apiKey must be non-empty");
+
+ Map headers = endpoint.headers();
+ String integrationId = headers.get("Copilot-Integration-Id");
+ assertNotNull(integrationId, "Copilot-Integration-Id header must be present");
+ assertFalse(integrationId.isEmpty(), "Copilot-Integration-Id must be non-empty");
+
+ String userAgent = headers.get("User-Agent");
+ assertNotNull(userAgent, "User-Agent header must be present");
+ assertTrue(userAgent.toLowerCase().contains("copilot"),
+ "expected User-Agent to mention Copilot, got " + userAgent);
+
+ String apiVersion = headers.get("X-GitHub-Api-Version");
+ assertNotNull(apiVersion, "X-GitHub-Api-Version header must be present");
+ assertFalse(apiVersion.isEmpty(), "X-GitHub-Api-Version must be non-empty");
+
+ String interactionId = headers.get("X-Interaction-Id");
+ assertNotNull(interactionId, "X-Interaction-Id header must be present");
+ assertTrue(interactionId.matches(".*[0-9a-f-]{8,}.*"),
+ "expected X-Interaction-Id to look like a hex/uuid value, got " + interactionId);
+
+ String authorization = headers.get("Authorization");
+ assertEquals("Bearer " + endpoint.apiKey(), authorization);
+
+ ProviderSessionToken sessionToken = endpoint.sessionToken();
+ if (sessionToken != null) {
+ assertEquals("Copilot-Session-Token", sessionToken.header());
+ assertFalse(sessionToken.token().isEmpty(), "session token must be non-empty");
+ // expiresAt is optional; when present it parses as OffsetDateTime so no
+ // additional validation is needed.
+ }
+ } finally {
+ session.close();
+ }
+ }
+ }
+}
diff --git a/nodejs/test/e2e/provider_endpoint.e2e.test.ts b/nodejs/test/e2e/provider_endpoint.e2e.test.ts
new file mode 100644
index 000000000..1bac76253
--- /dev/null
+++ b/nodejs/test/e2e/provider_endpoint.e2e.test.ts
@@ -0,0 +1,92 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+import { describe, expect, it } from "vitest";
+import { approveAll } from "../../src/index.js";
+import { createSdkTestContext } from "./harness/sdkTestContext.js";
+
+describe("session.provider.getEndpoint RPC", async () => {
+ const { copilotClient: client, env } = await createSdkTestContext();
+
+ // The provider endpoint API is gated behind an opt-in env var; the harness
+ // env object is the same one passed to the CLI subprocess, so mutating it
+ // here enables the API for this test file's client.
+ env.COPILOT_ALLOW_GET_PROVIDER_ENDPOINT = "true";
+
+ it("returns the BYOK provider endpoint when a custom provider is configured", async () => {
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ provider: {
+ type: "openai",
+ wireApi: "completions",
+ baseUrl: "https://api.example.test/v1",
+ apiKey: "byok-secret",
+ headers: { "X-Custom-Header": "byok-yes" },
+ },
+ });
+
+ try {
+ const endpoint = await session.rpc.provider.getEndpoint({});
+
+ expect(endpoint.type).toBe("openai");
+ expect(endpoint.wireApi).toBe("completions");
+ expect(endpoint.baseUrl).toBe("https://api.example.test/v1");
+ expect(endpoint.apiKey).toBe("byok-secret");
+ expect(endpoint.headers).toMatchObject({ "X-Custom-Header": "byok-yes" });
+ // BYOK sessions never issue a CAPI session token.
+ expect(endpoint.sessionToken).toBeUndefined();
+ } finally {
+ try {
+ await session.disconnect();
+ } catch {
+ // disconnect may fail since the BYOK provider URL is fake
+ }
+ }
+ });
+
+ it("returns the CAPI provider endpoint for an OAuth-authenticated session", async () => {
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ });
+
+ try {
+ const endpoint = await session.rpc.provider.getEndpoint({});
+
+ expect(["openai", "azure", "anthropic"]).toContain(endpoint.type);
+ // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes.
+ if (endpoint.type !== "anthropic") {
+ expect(["completions", "responses"]).toContain(endpoint.wireApi);
+ }
+
+ // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness.
+ expect(endpoint.baseUrl).toMatch(/^https?:\/\//);
+
+ // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer.
+ expect(endpoint.apiKey).toBeTypeOf("string");
+ expect(endpoint.apiKey!.length).toBeGreaterThan(0);
+
+ // Standard CAPI headers should be present, and Authorization is
+ // surfaced as the runtime sends it (`Bearer `).
+ expect(endpoint.headers["Copilot-Integration-Id"]).toBeTypeOf("string");
+ expect(endpoint.headers["User-Agent"]).toMatch(/Copilot/i);
+ expect(endpoint.headers["X-GitHub-Api-Version"]).toBeTypeOf("string");
+ expect(endpoint.headers["X-Interaction-Id"]).toMatch(/[0-9a-f-]{8,}/);
+ expect(endpoint.headers.Authorization).toBe(`Bearer ${endpoint.apiKey}`);
+
+ // When the omit-modelId path returned an auto-mode session token, it
+ // must use the documented header name and an ISO 8601 expiry. The
+ // harness may have a non-auto model selected, in which case the
+ // field is simply omitted.
+ if (endpoint.sessionToken) {
+ expect(endpoint.sessionToken.header).toBe("Copilot-Session-Token");
+ expect(endpoint.sessionToken.token.length).toBeGreaterThan(0);
+ if (endpoint.sessionToken.expiresAt !== undefined) {
+ expect(Date.parse(endpoint.sessionToken.expiresAt)).not.toBeNaN();
+ }
+ }
+ } finally {
+ await session.disconnect();
+ }
+ });
+});
diff --git a/python/e2e/test_provider_endpoint_e2e.py b/python/e2e/test_provider_endpoint_e2e.py
new file mode 100644
index 000000000..875a95b91
--- /dev/null
+++ b/python/e2e/test_provider_endpoint_e2e.py
@@ -0,0 +1,117 @@
+"""E2E tests for session.provider.getEndpoint."""
+
+# session.provider.getEndpoint is gated behind COPILOT_ALLOW_GET_PROVIDER_ENDPOINT;
+# the harness env passed to the CLI subprocess opts in for this test file.
+
+import re
+
+import pytest
+
+from copilot.client import CopilotClient, RuntimeConnection
+from copilot.generated.rpc import ProviderEndpointType, ProviderEndpointWireApi
+from copilot.session import PermissionHandler
+
+from .testharness import E2ETestContext
+
+pytestmark = pytest.mark.asyncio(loop_scope="module")
+
+
+@pytest.fixture(scope="module")
+async def provider_ctx(ctx: E2ETestContext):
+ env = {**ctx.get_env(), "COPILOT_ALLOW_GET_PROVIDER_ENDPOINT": "true"}
+ client = CopilotClient(
+ connection=RuntimeConnection.for_stdio(path=ctx.cli_path),
+ working_directory=ctx.work_dir,
+ env=env,
+ github_token=env["GITHUB_TOKEN"],
+ )
+ try:
+ yield ctx, client
+ finally:
+ await client.stop()
+
+
+class TestProviderEndpoint:
+ async def test_returns_byok_provider_endpoint_when_custom_provider_is_configured(
+ self, provider_ctx: tuple[E2ETestContext, CopilotClient]
+ ):
+ _, client = provider_ctx
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ provider={
+ "type": "openai",
+ "wire_api": "completions",
+ "base_url": "https://api.example.test/v1",
+ "api_key": "byok-secret",
+ "headers": {"X-Custom-Header": "byok-yes"},
+ },
+ )
+
+ try:
+ endpoint = await session.rpc.provider.get_endpoint()
+
+ assert endpoint.type == ProviderEndpointType.OPENAI
+ assert endpoint.wire_api == ProviderEndpointWireApi.COMPLETIONS
+ assert endpoint.base_url == "https://api.example.test/v1"
+ assert endpoint.api_key == "byok-secret"
+ assert endpoint.headers["X-Custom-Header"] == "byok-yes"
+ # BYOK sessions never issue a CAPI session token.
+ assert endpoint.session_token is None
+ finally:
+ try:
+ await session.disconnect()
+ except Exception:
+ pass # disconnect may fail since the BYOK provider URL is fake
+
+ async def test_returns_capi_provider_endpoint_for_oauth_authenticated_session(
+ self, provider_ctx: tuple[E2ETestContext, CopilotClient]
+ ):
+ _, client = provider_ctx
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ try:
+ endpoint = await session.rpc.provider.get_endpoint()
+
+ assert endpoint.type in (
+ ProviderEndpointType.OPENAI,
+ ProviderEndpointType.AZURE,
+ ProviderEndpointType.ANTHROPIC,
+ )
+ # wire_api is omitted for anthropic; otherwise one of the OpenAI shapes.
+ if endpoint.type != ProviderEndpointType.ANTHROPIC:
+ assert endpoint.wire_api in (
+ ProviderEndpointWireApi.COMPLETIONS,
+ ProviderEndpointWireApi.RESPONSES,
+ )
+
+ # CAPI baseUrl is the (proxy) Copilot API URL injected by the harness.
+ assert re.match(r"^https?://", endpoint.base_url)
+
+ # For CAPI OAuth sessions the api_key is the resolved GitHub bearer.
+ assert isinstance(endpoint.api_key, str)
+ assert len(endpoint.api_key) > 0
+
+ # Standard CAPI headers must be present, and Authorization is
+ # surfaced as the runtime sends it (`Bearer `).
+ assert isinstance(endpoint.headers["Copilot-Integration-Id"], str)
+ assert re.search(r"Copilot", endpoint.headers["User-Agent"], re.IGNORECASE)
+ assert isinstance(endpoint.headers["X-GitHub-Api-Version"], str)
+ assert re.search(r"[0-9a-f-]{8,}", endpoint.headers["X-Interaction-Id"])
+ assert endpoint.headers["Authorization"] == f"Bearer {endpoint.api_key}"
+
+ # When the omit-model_id path returned an auto-mode session token,
+ # it must use the documented header name. The harness may have a
+ # non-auto model selected, in which case the field is simply
+ # omitted.
+ if endpoint.session_token is not None:
+ assert endpoint.session_token.header == "Copilot-Session-Token"
+ assert len(endpoint.session_token.token) > 0
+ # When provided, expires_at should be a parseable ISO timestamp.
+ if endpoint.session_token.expires_at is not None:
+ from datetime import datetime
+
+ datetime.fromisoformat(endpoint.session_token.expires_at.replace("Z", "+00:00"))
+ finally:
+ await session.disconnect()
diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs
index 40bb5adb7..29137d13d 100644
--- a/rust/tests/e2e.rs
+++ b/rust/tests/e2e.rs
@@ -51,6 +51,8 @@ mod per_session_auth;
mod permissions;
#[path = "e2e/pre_mcp_tool_call_hook.rs"]
mod pre_mcp_tool_call_hook;
+#[path = "e2e/provider_endpoint.rs"]
+mod provider_endpoint;
#[path = "e2e/rpc_additional_edge_cases.rs"]
mod rpc_additional_edge_cases;
#[path = "e2e/rpc_agent.rs"]
diff --git a/rust/tests/e2e/provider_endpoint.rs b/rust/tests/e2e/provider_endpoint.rs
new file mode 100644
index 000000000..df6d5941d
--- /dev/null
+++ b/rust/tests/e2e/provider_endpoint.rs
@@ -0,0 +1,215 @@
+use std::collections::HashMap;
+use std::ffi::OsString;
+use std::sync::Arc;
+
+use github_copilot_sdk::handler::ApproveAllHandler;
+use github_copilot_sdk::rpc::{ProviderEndpointType, ProviderEndpointWireApi};
+use github_copilot_sdk::{ProviderConfig, SessionConfig};
+
+use super::support::{DEFAULT_TEST_TOKEN, with_e2e_context};
+
+// session.provider.getEndpoint is gated behind COPILOT_ALLOW_GET_PROVIDER_ENDPOINT;
+// the harness env passed to the CLI subprocess opts in for these tests.
+fn opt_in_env() -> (OsString, OsString) {
+ ("COPILOT_ALLOW_GET_PROVIDER_ENDPOINT".into(), "true".into())
+}
+
+#[tokio::test]
+async fn byok_provider_endpoint_returns_configured_endpoint() {
+ with_e2e_context(
+ "provider-endpoint",
+ "byok_provider_endpoint_returns_configured_endpoint",
+ |ctx| {
+ Box::pin(async move {
+ let mut options = ctx.client_options();
+ options.env.push(opt_in_env());
+ let client = github_copilot_sdk::Client::start(options)
+ .await
+ .expect("start client");
+
+ let mut headers = HashMap::new();
+ headers.insert("X-Custom-Header".to_string(), "byok-yes".to_string());
+
+ let session = client
+ .create_session(
+ SessionConfig::default()
+ .with_permission_handler(Arc::new(ApproveAllHandler))
+ .with_provider(
+ ProviderConfig::new("https://api.example.test/v1")
+ .with_provider_type("openai")
+ .with_wire_api("completions")
+ .with_api_key("byok-secret")
+ .with_headers(headers),
+ ),
+ )
+ .await
+ .expect("create session");
+
+ let endpoint = session
+ .rpc()
+ .provider()
+ .get_endpoint()
+ .await
+ .expect("get_endpoint");
+
+ assert!(
+ matches!(endpoint.r#type, ProviderEndpointType::Openai),
+ "expected type=openai, got {:?}",
+ endpoint.r#type,
+ );
+ assert!(
+ matches!(
+ endpoint.wire_api,
+ Some(ProviderEndpointWireApi::Completions)
+ ),
+ "expected wireApi=completions, got {:?}",
+ endpoint.wire_api,
+ );
+ assert_eq!(endpoint.base_url, "https://api.example.test/v1");
+ assert_eq!(endpoint.api_key.as_deref(), Some("byok-secret"));
+ assert_eq!(
+ endpoint.headers.get("X-Custom-Header").map(String::as_str),
+ Some("byok-yes"),
+ );
+ assert!(
+ endpoint.session_token.is_none(),
+ "BYOK sessions never issue a CAPI session token",
+ );
+
+ // disconnect may fail since the BYOK provider URL is fake
+ let _ = session.disconnect().await;
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn capi_provider_endpoint_returns_resolved_credentials() {
+ with_e2e_context(
+ "provider-endpoint",
+ "capi_provider_endpoint_returns_resolved_credentials",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let mut options = ctx.client_options().with_github_token(DEFAULT_TEST_TOKEN);
+ options.env.push(opt_in_env());
+ let client = github_copilot_sdk::Client::start(options)
+ .await
+ .expect("start client");
+
+ let session = client
+ .create_session(
+ SessionConfig::default()
+ .with_permission_handler(Arc::new(ApproveAllHandler)),
+ )
+ .await
+ .expect("create session");
+
+ let endpoint = session
+ .rpc()
+ .provider()
+ .get_endpoint()
+ .await
+ .expect("get_endpoint");
+
+ assert!(
+ matches!(
+ endpoint.r#type,
+ ProviderEndpointType::Openai
+ | ProviderEndpointType::Azure
+ | ProviderEndpointType::Anthropic
+ ),
+ "expected type in {{openai, azure, anthropic}}, got {:?}",
+ endpoint.r#type,
+ );
+ if !matches!(endpoint.r#type, ProviderEndpointType::Anthropic) {
+ assert!(
+ matches!(
+ endpoint.wire_api,
+ Some(ProviderEndpointWireApi::Completions)
+ | Some(ProviderEndpointWireApi::Responses)
+ ),
+ "expected wireApi in {{completions, responses}}, got {:?}",
+ endpoint.wire_api,
+ );
+ }
+
+ assert!(
+ endpoint.base_url.starts_with("http://")
+ || endpoint.base_url.starts_with("https://"),
+ "expected http(s) baseUrl, got {}",
+ endpoint.base_url,
+ );
+
+ let api_key = endpoint
+ .api_key
+ .as_deref()
+ .expect("CAPI OAuth session must surface apiKey");
+ assert!(!api_key.is_empty(), "apiKey must be non-empty");
+
+ let integration_id = endpoint
+ .headers
+ .get("Copilot-Integration-Id")
+ .expect("Copilot-Integration-Id header");
+ assert!(
+ !integration_id.is_empty(),
+ "Copilot-Integration-Id must be non-empty",
+ );
+
+ let user_agent = endpoint
+ .headers
+ .get("User-Agent")
+ .expect("User-Agent header");
+ assert!(
+ user_agent.to_ascii_lowercase().contains("copilot"),
+ "expected User-Agent to mention Copilot, got {user_agent}",
+ );
+
+ let api_version = endpoint
+ .headers
+ .get("X-GitHub-Api-Version")
+ .expect("X-GitHub-Api-Version header");
+ assert!(
+ !api_version.is_empty(),
+ "X-GitHub-Api-Version must be non-empty",
+ );
+
+ let interaction_id = endpoint
+ .headers
+ .get("X-Interaction-Id")
+ .expect("X-Interaction-Id header");
+ let hex_count = interaction_id
+ .chars()
+ .filter(|c| c.is_ascii_hexdigit() || *c == '-')
+ .count();
+ assert!(
+ hex_count >= 8,
+ "expected X-Interaction-Id to look like a hex/uuid value, got {interaction_id}",
+ );
+
+ let authorization = endpoint
+ .headers
+ .get("Authorization")
+ .expect("Authorization header");
+ assert_eq!(authorization, &format!("Bearer {api_key}"));
+
+ if let Some(session_token) = endpoint.session_token.as_ref() {
+ assert_eq!(session_token.header, "Copilot-Session-Token");
+ assert!(
+ !session_token.token.is_empty(),
+ "session token must be non-empty",
+ );
+ if let Some(expires_at) = session_token.expires_at.as_deref() {
+ assert!(!expires_at.is_empty(), "expected non-empty expiresAt",);
+ }
+ }
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}