From 4e3eca12f0e11b76d7d5d02edd25292f2497ee0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:36:00 +0000 Subject: [PATCH 1/4] Initial plan From 991d1e5598d867d25b2e5a567a75da532b4c5d02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:48:47 +0000 Subject: [PATCH 2/4] Port upstream sync: GitHubToken rename, sendAndWait cancellation fix, Foundry Local docs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CliServerManager.java | 10 ++--- .../com/github/copilot/sdk/CopilotClient.java | 4 +- .../github/copilot/sdk/CopilotSession.java | 20 ++++++++- .../sdk/json/CopilotClientOptions.java | 39 +++++++++++++--- src/site/markdown/advanced.md | 45 +++++++++++++++++++ src/site/markdown/setup.md | 12 ++--- .../copilot/sdk/CliServerManagerTest.java | 10 ++--- .../github/copilot/sdk/ConfigCloneTest.java | 4 ++ .../github/copilot/sdk/CopilotClientTest.java | 14 +++--- .../github/copilot/sdk/E2ETestContext.java | 2 +- 10 files changed, 125 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 42e41245e..b2a798ada 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -70,15 +70,15 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { } // Add auth-related flags - if (options.getGithubToken() != null && !options.getGithubToken().isEmpty()) { + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { args.add("--auth-token-env"); args.add("COPILOT_SDK_AUTH_TOKEN"); } - // Default UseLoggedInUser to false when GithubToken is provided + // Default UseLoggedInUser to false when GitHubToken is provided boolean useLoggedInUser = options.getUseLoggedInUser() != null ? options.getUseLoggedInUser() - : (options.getGithubToken() == null || options.getGithubToken().isEmpty()); + : (options.getGitHubToken() == null || options.getGitHubToken().isEmpty()); if (!useLoggedInUser) { args.add("--no-auto-login"); } @@ -106,8 +106,8 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.environment().remove("NODE_DEBUG"); // Set auth token in environment if provided - if (options.getGithubToken() != null && !options.getGithubToken().isEmpty()) { - pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGithubToken()); + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { + pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); } Process process = pb.start(); diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 8d4806874..043483284 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -110,9 +110,9 @@ public CopilotClient(CopilotClientOptions options) { // Validate auth options with external server if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty() - && (this.options.getGithubToken() != null || this.options.getUseLoggedInUser() != null)) { + && (this.options.getGitHubToken() != null || this.options.getUseLoggedInUser() != null)) { throw new IllegalArgumentException( - "GithubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); + "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); } // Parse CliUrl if provided diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 722407c50..d40c2d3f1 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -312,6 +312,12 @@ public CompletableFuture send(MessageOptions options) { * This method blocks until the assistant finishes processing the message or * until the timeout expires. It's suitable for simple request/response * interactions where you don't need to process streaming events. + *

+ * The returned future can be cancelled via + * {@link java.util.concurrent.Future#cancel(boolean)}. If cancelled externally, + * the future completes with {@link java.util.concurrent.CancellationException}. + * If the timeout expires first, the future completes exceptionally with a + * {@link TimeoutException}. * * @param options * the message options containing the prompt and attachments @@ -320,7 +326,7 @@ public CompletableFuture send(MessageOptions options) { * @return a future that resolves with the final assistant message event, or * {@code null} if no assistant message was received. The future * completes exceptionally with a TimeoutException if the timeout - * expires. + * expires, or with CancellationException if cancelled externally. * @throws IllegalStateException * if this session has been terminated * @see #sendAndWait(MessageOptions) @@ -367,7 +373,7 @@ public CompletableFuture sendAndWait(MessageOptions optio scheduler.shutdown(); }, timeoutMs, TimeUnit.MILLISECONDS); - return future.whenComplete((result, ex) -> { + var resultFuture = future.whenComplete((result, ex) -> { try { subscription.close(); } catch (IOException e) { @@ -375,6 +381,16 @@ public CompletableFuture sendAndWait(MessageOptions optio } scheduler.shutdown(); }); + + // When the returned future is cancelled externally, propagate to the inner + // future so that cleanup (subscription close, scheduler shutdown) runs. + resultFuture.whenComplete((v, ex) -> { + if (resultFuture.isCancelled()) { + future.completeExceptionally(new java.util.concurrent.CancellationException()); + } + }); + + return resultFuture; } /** diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index d83b8b6eb..70ce99850 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -41,7 +41,7 @@ public class CopilotClientOptions { private boolean autoStart = true; private boolean autoRestart = true; private Map environment; - private String githubToken; + private String gitHubToken; private Boolean useLoggedInUser; /** @@ -279,8 +279,8 @@ public CopilotClientOptions setEnvironment(Map environment) { * * @return the GitHub token, or {@code null} to use other authentication methods */ - public String getGithubToken() { - return githubToken; + public String getGitHubToken() { + return gitHubToken; } /** @@ -289,12 +289,37 @@ public String getGithubToken() { * When provided, the token is passed to the CLI server via environment * variable. This takes priority over other authentication methods. * + * @param gitHubToken + * the GitHub token + * @return this options instance for method chaining + */ + public CopilotClientOptions setGitHubToken(String gitHubToken) { + this.gitHubToken = gitHubToken; + return this; + } + + /** + * Gets the GitHub token for authentication. + * + * @return the GitHub token, or {@code null} to use other authentication methods + * @deprecated Use {@link #getGitHubToken()} instead. + */ + @Deprecated + public String getGithubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token to use for authentication. + * * @param githubToken * the GitHub token * @return this options instance for method chaining + * @deprecated Use {@link #setGitHubToken(String)} instead. */ + @Deprecated public CopilotClientOptions setGithubToken(String githubToken) { - this.githubToken = githubToken; + this.gitHubToken = githubToken; return this; } @@ -312,8 +337,8 @@ public Boolean getUseLoggedInUser() { * Sets whether to use the logged-in user for authentication. *

* When true, the CLI server will attempt to use stored OAuth tokens or gh CLI - * auth. When false, only explicit tokens (githubToken or environment variables) - * are used. Default: true (but defaults to false when githubToken is provided). + * auth. When false, only explicit tokens (gitHubToken or environment variables) + * are used. Default: true (but defaults to false when gitHubToken is provided). * * @param useLoggedInUser * {@code true} to use logged-in user auth, {@code false} otherwise @@ -347,7 +372,7 @@ public CopilotClientOptions clone() { copy.autoStart = this.autoStart; copy.autoRestart = this.autoRestart; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; - copy.githubToken = this.githubToken; + copy.gitHubToken = this.gitHubToken; copy.useLoggedInUser = this.useLoggedInUser; return copy; } diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 0c43f347e..66339d30e 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -149,6 +149,17 @@ session.send(new MessageOptions() Use your own OpenAI or Azure OpenAI API key instead of GitHub Copilot. +Supported providers: + +| Provider | Type value | Notes | +|----------|-----------|-------| +| OpenAI | `"openai"` | Standard OpenAI API | +| Azure OpenAI / Azure AI Foundry | `"azure"` | Azure-hosted models | +| Anthropic | `"anthropic"` | Claude models | +| Ollama | `"openai"` | Local models via OpenAI-compatible API | +| Microsoft Foundry Local | `"openai"` | Run AI models locally on your device via OpenAI-compatible API | +| Other OpenAI-compatible | `"openai"` | vLLM, LiteLLM, etc. | + ### API Key Authentication ```java @@ -177,6 +188,40 @@ var session = client.createSession( > **Note:** The `bearerToken` option accepts a **static token string** only. The SDK does not refresh this token automatically. If your token expires, requests will fail and you'll need to create a new session with a fresh token. +### Microsoft Foundry Local + +[Microsoft Foundry Local](https://foundrylocal.ai) lets you run AI models locally on your own device with an OpenAI-compatible API. Install it via the Foundry Local CLI, then point the SDK at your local endpoint: + +```java +var session = client.createSession( + new SessionConfig() + .setProvider(new ProviderConfig() + .setType("openai") + .setBaseUrl("http://localhost:/v1")) + // No apiKey needed for local Foundry Local +).get(); +``` + +> **Note:** Foundry Local starts on a **dynamic port** — the port is not fixed. Use `foundry service status` to confirm the port the service is currently listening on, then use that port in your `baseUrl`. + +To get started with Foundry Local: + +```bash +# Windows: Install Foundry Local CLI (requires winget) +winget install Microsoft.FoundryLocal + +# macOS / Linux: see https://foundrylocal.ai for installation instructions + +# List available models +foundry model list + +# Run a model (starts the local server automatically) +foundry model run phi-4-mini + +# Check the port the service is running on +foundry service status +``` + ### Limitations When using BYOK, be aware of these limitations: diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index eebec5bcd..1130808ff 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -7,7 +7,7 @@ This guide explains how to configure the Copilot SDK for different deployment sc | Scenario | Configuration | Guide Section | |----------|--------------|---------------| | Local development | Default (no options) | [Local CLI](#local-cli) | -| Multi-user app | `setGithubToken(userToken)` | [GitHub OAuth](#github-oauth-authentication) | +| Multi-user app | `setGitHubToken(userToken)` | [GitHub OAuth](#github-oauth-authentication) | | Server deployment | `setCliUrl("host:port")` | [Backend Services](#backend-services) | | Custom CLI location | `setCliPath("/path/to/copilot")` | [Bundled CLI](#bundled-cli) | | Own model keys | Provider configuration | [BYOK](advanced.html#Bring_Your_Own_Key_BYOK) | @@ -48,7 +48,7 @@ After obtaining a user's GitHub OAuth token, pass it to the SDK: ```java var options = new CopilotClientOptions() - .setGithubToken(userAccessToken) + .setGitHubToken(userAccessToken) .setUseLoggedInUser(false); try (var client = new CopilotClient(options)) { @@ -67,7 +67,7 @@ Your application handles the OAuth flow: 1. Create a GitHub OAuth App in your GitHub settings 2. Redirect users to GitHub's authorization URL 3. Exchange the authorization code for an access token -4. Pass the token to `CopilotClientOptions.setGithubToken()` +4. Pass the token to `CopilotClientOptions.setGitHubToken()` ### Per-User Client Management @@ -76,10 +76,10 @@ Each authenticated user should get their own client instance: ```java private final Map clients = new ConcurrentHashMap<>(); -public CopilotClient getClientForUser(String userId, String githubToken) { +public CopilotClient getClientForUser(String userId, String gitHubToken) { return clients.computeIfAbsent(userId, id -> { var options = new CopilotClientOptions() - .setGithubToken(githubToken) + .setGitHubToken(gitHubToken) .setUseLoggedInUser(false); var client = new CopilotClient(options); try { @@ -340,7 +340,7 @@ Complete list of `CopilotClientOptions` settings: | `cliPath` | String | Path to CLI executable | `"copilot"` from PATH | | `cliUrl` | String | External CLI server URL | `null` (spawn process) | | `cliArgs` | String[] | Extra CLI arguments | `null` | -| `githubToken` | String | GitHub OAuth token | `null` | +| `gitHubToken` | String | GitHub OAuth token | `null` | | `useLoggedInUser` | Boolean | Use system credentials | `true` | | `useStdio` | boolean | Use stdio transport | `true` | | `port` | int | TCP port for CLI | `0` (random) | diff --git a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java index 61ce938cf..86d6be875 100644 --- a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java +++ b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java @@ -165,9 +165,9 @@ void startCliServerWithExplicitPort() throws Exception { } @Test - void startCliServerWithGithubToken() throws Exception { + void startCliServerWithGitHubToken() throws Exception { // Test the github token branch - var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGithubToken("ghp_test123") + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGitHubToken("ghp_test123") .setUseStdio(true); var manager = new CliServerManager(options); @@ -187,9 +187,9 @@ void startCliServerWithUseLoggedInUserExplicit() throws Exception { } @Test - void startCliServerWithGithubTokenAndNoExplicitUseLoggedInUser() throws Exception { - // When githubToken is set and useLoggedInUser is null, defaults to false - var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGithubToken("ghp_test123") + void startCliServerWithGitHubTokenAndNoExplicitUseLoggedInUser() throws Exception { + // When gitHubToken is set and useLoggedInUser is null, defaults to false + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGitHubToken("ghp_test123") .setUseStdio(true); var manager = new CliServerManager(options); diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index 1c5ca6e09..e1269a669 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -26,12 +26,16 @@ void copilotClientOptionsCloneBasic() { original.setCliPath("/usr/local/bin/copilot"); original.setLogLevel("debug"); original.setPort(9000); + original.setGitHubToken("ghp_test"); + original.setUseLoggedInUser(false); CopilotClientOptions cloned = original.clone(); assertEquals(original.getCliPath(), cloned.getCliPath()); assertEquals(original.getLogLevel(), cloned.getLogLevel()); assertEquals(original.getPort(), cloned.getPort()); + assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); + assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); } @Test diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 514e68998..a5060e615 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -203,10 +203,10 @@ void testForceStopWithoutCleanup() throws Exception { } @Test - void testGithubTokenOptionAccepted() { - var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGithubToken("gho_test_token"); + void testGitHubTokenOptionAccepted() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token"); - assertEquals("gho_test_token", options.getGithubToken()); + assertEquals("gho_test_token", options.getGitHubToken()); } @Test @@ -224,16 +224,16 @@ void testExplicitUseLoggedInUserFalse() { } @Test - void testExplicitUseLoggedInUserTrueWithGithubToken() { - var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGithubToken("gho_test_token") + void testExplicitUseLoggedInUserTrueWithGitHubToken() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token") .setUseLoggedInUser(true); assertEquals(true, options.getUseLoggedInUser()); } @Test - void testGithubTokenWithCliUrlThrows() { - var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGithubToken("gho_test_token"); + void testGitHubTokenWithCliUrlThrows() { + var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGitHubToken("gho_test_token"); assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); } diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 0ec14dc0b..6bb351901 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -265,7 +265,7 @@ public CopilotClient createClient() { // In CI, use a fake token to avoid auth issues String ci = System.getenv("CI"); if (ci != null && !ci.isEmpty()) { - options.setGithubToken("fake-token-for-e2e-tests"); + options.setGitHubToken("fake-token-for-e2e-tests"); } return new CopilotClient(options); From 2076705914b4863c0a45ac240a53dd62c5594fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:49:09 +0000 Subject: [PATCH 3/4] Update .lastmerge to f0909a78ce6c242f1f07e91b72ab7a7d8c910531 --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index d5389b23c..4d56ec15e 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -c4b3b366c4bd8dfba9ba4aa05e4019825360ad78 +f0909a78ce6c242f1f07e91b72ab7a7d8c910531 From aebf4515a0d7aff6f80b9df494ab4d89a6f50ea0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:53:40 +0000 Subject: [PATCH 4/4] Fix sendAndWait cancellation propagation and Foundry Local docs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CopilotSession.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index d40c2d3f1..e7790ea54 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -373,24 +373,33 @@ public CompletableFuture sendAndWait(MessageOptions optio scheduler.shutdown(); }, timeoutMs, TimeUnit.MILLISECONDS); - var resultFuture = future.whenComplete((result, ex) -> { + var result = new CompletableFuture(); + + // When inner future completes, run cleanup and propagate to result + future.whenComplete((r, ex) -> { try { subscription.close(); } catch (IOException e) { LOG.log(Level.SEVERE, "Error closing subscription", e); } scheduler.shutdown(); + if (!result.isDone()) { + if (ex != null) { + result.completeExceptionally(ex); + } else { + result.complete(r); + } + } }); - // When the returned future is cancelled externally, propagate to the inner - // future so that cleanup (subscription close, scheduler shutdown) runs. - resultFuture.whenComplete((v, ex) -> { - if (resultFuture.isCancelled()) { - future.completeExceptionally(new java.util.concurrent.CancellationException()); + // When result is cancelled externally, cancel inner future to trigger cleanup + result.whenComplete((v, ex) -> { + if (result.isCancelled() && !future.isDone()) { + future.cancel(true); } }); - return resultFuture; + return result; } /**