Merge c800a686e7 into da71eaebd2
This commit is contained in:
commit
d71bf783da
@ -3,6 +3,7 @@ summary: "Sign in to GitHub Copilot from OpenClaw using the device flow"
|
||||
read_when:
|
||||
- You want to use GitHub Copilot as a model provider
|
||||
- You need the `openclaw models auth login-github-copilot` flow
|
||||
- You already use the GitHub Copilot CLI
|
||||
---
|
||||
# Github Copilot
|
||||
|
||||
@ -10,9 +11,9 @@ read_when:
|
||||
|
||||
GitHub Copilot is GitHub's AI coding assistant. It provides access to Copilot
|
||||
models for your GitHub account and plan. OpenClaw can use Copilot as a model
|
||||
provider in two different ways.
|
||||
provider in three different ways.
|
||||
|
||||
## Two ways to use Copilot in OpenClaw
|
||||
## Three ways to use Copilot in OpenClaw
|
||||
|
||||
### 1) Built-in GitHub Copilot provider (`github-copilot`)
|
||||
|
||||
@ -20,10 +21,40 @@ Use the native device-login flow to obtain a GitHub token, then exchange it for
|
||||
Copilot API tokens when OpenClaw runs. This is the **default** and simplest path
|
||||
because it does not require VS Code.
|
||||
|
||||
### 2) Copilot Proxy plugin (`copilot-proxy`)
|
||||
### 2) Copilot CLI token (`gho_` tokens)
|
||||
|
||||
If you already use the [GitHub Copilot CLI](https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line),
|
||||
you can use its OAuth token directly. These tokens (prefixed with `gho_`) work
|
||||
with the Copilot API without any additional exchange.
|
||||
|
||||
\`\`\`bash
|
||||
# Install and authenticate the Copilot CLI
|
||||
npm install -g @github/copilot
|
||||
copilot auth login
|
||||
|
||||
# Get your token from ~/.copilot/config.json and set it.
|
||||
# The file structure looks like:
|
||||
# {
|
||||
# "github.com": {
|
||||
# "token": "gho_your_token_here",
|
||||
# "user": "your_username"
|
||||
# }
|
||||
# }
|
||||
# Use the value of github.com.token:
|
||||
export COPILOT_GITHUB_TOKEN="gho_your_token_here"
|
||||
\`\`\`
|
||||
|
||||
For **enterprise users** whose proxy blocks the individual endpoint, set the
|
||||
base URL to your enterprise endpoint:
|
||||
|
||||
\`\`\`bash
|
||||
export COPILOT_API_BASE_URL="https://api.business.githubcopilot.com"
|
||||
\`\`\`
|
||||
|
||||
### 3) Copilot Proxy plugin (`copilot-proxy`)
|
||||
|
||||
Use the **Copilot Proxy** VS Code extension as a local bridge. OpenClaw talks to
|
||||
the proxy’s `/v1` endpoint and uses the model list you configure there. Choose
|
||||
the proxy's `/v1` endpoint and uses the model list you configure there. Choose
|
||||
this when you already run Copilot Proxy in VS Code or need to route through it.
|
||||
You must enable the plugin and keep the VS Code extension running.
|
||||
|
||||
@ -33,38 +64,38 @@ profile.
|
||||
|
||||
## CLI setup
|
||||
|
||||
```bash
|
||||
\`\`\`bash
|
||||
openclaw models auth login-github-copilot
|
||||
```
|
||||
\`\`\`
|
||||
|
||||
You'll be prompted to visit a URL and enter a one-time code. Keep the terminal
|
||||
open until it completes.
|
||||
|
||||
### Optional flags
|
||||
|
||||
```bash
|
||||
\`\`\`bash
|
||||
openclaw models auth login-github-copilot --profile-id github-copilot:work
|
||||
openclaw models auth login-github-copilot --yes
|
||||
```
|
||||
\`\`\`
|
||||
|
||||
## Set a default model
|
||||
|
||||
```bash
|
||||
\`\`\`bash
|
||||
openclaw models set github-copilot/gpt-4o
|
||||
```
|
||||
\`\`\`
|
||||
|
||||
### Config snippet
|
||||
|
||||
```json5
|
||||
\`\`\`json5
|
||||
{
|
||||
agents: { defaults: { model: { primary: "github-copilot/gpt-4o" } } }
|
||||
}
|
||||
```
|
||||
\`\`\`
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires an interactive TTY; run it directly in a terminal.
|
||||
- Copilot model availability depends on your plan; if a model is rejected, try
|
||||
another ID (for example `github-copilot/gpt-4.1`).
|
||||
another ID (for example \`github-copilot/gpt-4.1\`).
|
||||
- The login stores a GitHub token in the auth profile store and exchanges it for a
|
||||
Copilot API token when OpenClaw runs.
|
||||
|
||||
@ -26,6 +26,7 @@ const DEFAULT_REDACT_PATTERNS: string[] = [
|
||||
// Common token prefixes.
|
||||
String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
||||
String.raw`\b(ghp_[A-Za-z0-9]{20,})\b`,
|
||||
String.raw`\b(gho_[A-Za-z0-9]{20,})\b`,
|
||||
String.raw`\b(github_pat_[A-Za-z0-9_]{20,})\b`,
|
||||
String.raw`\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`,
|
||||
String.raw`\b(xapp-[A-Za-z0-9-]{10,})\b`,
|
||||
|
||||
@ -78,4 +78,70 @@ describe("github-copilot token", () => {
|
||||
expect(res.baseUrl).toBe("https://api.contoso.test");
|
||||
expect(saveJsonFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses gho_ tokens directly without exchange", async () => {
|
||||
loadJsonFile.mockReturnValue(undefined);
|
||||
|
||||
const fetchImpl = vi.fn();
|
||||
|
||||
const { resolveCopilotApiToken } = await import("./github-copilot-token.js");
|
||||
|
||||
const res = await resolveCopilotApiToken({
|
||||
githubToken: "gho_testtoken123456789",
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
// gho_ tokens should be used directly
|
||||
expect(res.token).toBe("gho_testtoken123456789");
|
||||
expect(res.source).toBe("copilot-cli:direct");
|
||||
expect(res.baseUrl).toBe("https://api.individual.githubcopilot.com");
|
||||
// Should NOT call the exchange endpoint
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
// Should cache the token
|
||||
expect(saveJsonFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("exchanges non-gho tokens via API", async () => {
|
||||
loadJsonFile.mockReturnValue(undefined);
|
||||
|
||||
const fetchImpl = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
token: "exchanged;proxy-ep=https://proxy.github.test;",
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||
}),
|
||||
});
|
||||
|
||||
const { resolveCopilotApiToken } = await import("./github-copilot-token.js");
|
||||
|
||||
const res = await resolveCopilotApiToken({
|
||||
githubToken: "ghp_regularoauthtoken",
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
// Non-gho tokens should go through the exchange
|
||||
expect(res.token).toBe("exchanged;proxy-ep=https://proxy.github.test;");
|
||||
expect(String(res.source)).toContain("fetched:");
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses COPILOT_API_BASE_URL env var for gho_ tokens", async () => {
|
||||
loadJsonFile.mockReturnValue(undefined);
|
||||
|
||||
const fetchImpl = vi.fn();
|
||||
|
||||
const { resolveCopilotApiToken } = await import("./github-copilot-token.js");
|
||||
|
||||
const res = await resolveCopilotApiToken({
|
||||
githubToken: "gho_enterprisetoken",
|
||||
env: { COPILOT_API_BASE_URL: "https://api.business.githubcopilot.com" },
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
// Should use the env var for baseUrl
|
||||
expect(res.token).toBe("gho_enterprisetoken");
|
||||
expect(res.baseUrl).toBe("https://api.business.githubcopilot.com");
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -73,6 +73,39 @@ export function deriveCopilotApiBaseUrlFromToken(token: string): string | null {
|
||||
return `https://${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is a GitHub OAuth token from the Copilot CLI.
|
||||
* These tokens (prefixed with `gho_`) can be used directly with the Copilot API
|
||||
* without requiring a token exchange via /copilot_internal/v2/token.
|
||||
*/
|
||||
function isCopilotCliToken(token: string): boolean {
|
||||
return token.startsWith("gho_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Default cache TTL for Copilot CLI OAuth tokens (prefixed with `gho_`).
|
||||
*
|
||||
* As of this writing, the Copilot CLI issues `gho_` tokens with an 8-hour lifetime,
|
||||
* so we cache them for the same duration. GitHub OAuth token lifetimes can be
|
||||
* configured and may change over time; this value is therefore a best-effort
|
||||
* approximation for cache expiry rather than a guaranteed reflection of the
|
||||
* server-side token lifetime.
|
||||
*/
|
||||
const COPILOT_CLI_TOKEN_TTL_MS = 8 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Resolve the Copilot API base URL.
|
||||
* Checks COPILOT_API_BASE_URL env var first (for enterprise/custom endpoints),
|
||||
* then falls back to the default individual endpoint.
|
||||
*/
|
||||
function resolveCopilotApiBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const envUrl = env.COPILOT_API_BASE_URL?.trim();
|
||||
if (envUrl) {
|
||||
return envUrl;
|
||||
}
|
||||
return DEFAULT_COPILOT_API_BASE_URL;
|
||||
}
|
||||
|
||||
export async function resolveCopilotApiToken(params: {
|
||||
githubToken: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@ -97,6 +130,25 @@ export async function resolveCopilotApiToken(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Copilot CLI tokens (gho_*) can be used directly with the Copilot API.
|
||||
// These tokens are already authenticated and don't need the /v2/token exchange.
|
||||
// See: https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line
|
||||
if (isCopilotCliToken(params.githubToken)) {
|
||||
const now = Date.now();
|
||||
const payload: CachedCopilotToken = {
|
||||
token: params.githubToken,
|
||||
expiresAt: now + COPILOT_CLI_TOKEN_TTL_MS,
|
||||
updatedAt: now,
|
||||
};
|
||||
saveJsonFile(cachePath, payload);
|
||||
return {
|
||||
token: payload.token,
|
||||
expiresAt: payload.expiresAt,
|
||||
source: "copilot-cli:direct",
|
||||
baseUrl: resolveCopilotApiBaseUrl(env),
|
||||
};
|
||||
}
|
||||
|
||||
const fetchImpl = params.fetchImpl ?? fetch;
|
||||
const res = await fetchImpl(COPILOT_TOKEN_URL, {
|
||||
method: "GET",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user