Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .lastmerge
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c4b3b366c4bd8dfba9ba4aa05e4019825360ad78
f0909a78ce6c242f1f07e91b72ab7a7d8c910531
10 changes: 5 additions & 5 deletions src/main/java/com/github/copilot/sdk/CliServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ public CompletableFuture<String> 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.
* <p>
* 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
Expand All @@ -320,7 +326,7 @@ public CompletableFuture<String> 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)
Expand Down Expand Up @@ -367,14 +373,33 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
scheduler.shutdown();
}, timeoutMs, TimeUnit.MILLISECONDS);

return future.whenComplete((result, ex) -> {
var result = new CompletableFuture<AssistantMessageEvent>();

// 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 result is cancelled externally, cancel inner future to trigger cleanup
result.whenComplete((v, ex) -> {
if (result.isCancelled() && !future.isDone()) {
future.cancel(true);
}
});

return result;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class CopilotClientOptions {
private boolean autoStart = true;
private boolean autoRestart = true;
private Map<String, String> environment;
private String githubToken;
private String gitHubToken;
private Boolean useLoggedInUser;

/**
Expand Down Expand Up @@ -279,8 +279,8 @@ public CopilotClientOptions setEnvironment(Map<String, String> environment) {
*
* @return the GitHub token, or {@code null} to use other authentication methods
*/
public String getGithubToken() {
return githubToken;
public String getGitHubToken() {
return gitHubToken;
}

/**
Expand All @@ -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;
}

Expand All @@ -312,8 +337,8 @@ public Boolean getUseLoggedInUser() {
* Sets whether to use the logged-in user for authentication.
* <p>
* 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
Expand Down Expand Up @@ -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;
}
Expand Down
45 changes: 45 additions & 0 deletions src/site/markdown/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:<PORT>/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:
Expand Down
12 changes: 6 additions & 6 deletions src/site/markdown/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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)) {
Expand All @@ -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

Expand All @@ -76,10 +76,10 @@ Each authenticated user should get their own client instance:
```java
private final Map<String, CopilotClient> 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 {
Expand Down Expand Up @@ -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) |
Expand Down
10 changes: 5 additions & 5 deletions src/test/java/com/github/copilot/sdk/CliServerManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions src/test/java/com/github/copilot/sdk/CopilotClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/com/github/copilot/sdk/E2ETestContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading