chore: remove deprecated copilot-proxy extension; add github-copilot-token helper; adjust default config candidates; update docs

This commit is contained in:
Aditya Mer 2026-01-30 12:11:59 +05:30
parent 2719e7b626
commit 1f7e1ca5d5
9 changed files with 89 additions and 224 deletions

View File

@ -47,7 +47,7 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
- Google Antigravity OAuth (provider auth) — bundled as `google-antigravity-auth` (disabled by default)
- Gemini CLI OAuth (provider auth) — bundled as `google-gemini-cli-auth` (disabled by default)
- Qwen OAuth (provider auth) — bundled as `qwen-portal-auth` (disabled by default)
- Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in `github-copilot` device login (bundled, disabled by default)
OpenClaw plugins are **TypeScript modules** loaded at runtime via jiti. **Config
validation does not execute plugin code**; it uses the plugin manifest and JSON

View File

@ -11,22 +11,10 @@ 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.
## Two ways to use Copilot in OpenClaw
### 1) Device flow (recommended)
Use the native GitHub 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`)
Use the **Copilot Proxy** VS Code extension as a local bridge. OpenClaw talks to
the proxys `/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.
provider via the official GitHub Copilot SDK. OpenClaw uses the GitHub device
flow to obtain a GitHub token and exchanges it for Copilot API tokens at runtime.
This is the recommended and supported path for integrating Copilot with
OpenClaw.
Use GitHub Copilot as a model provider (`github-copilot`). The login command runs
the GitHub device flow, saves an auth profile, and updates your config to use

View File

@ -1,24 +0,0 @@
# Copilot Proxy (OpenClaw plugin)
Provider plugin for the **Copilot Proxy** VS Code extension.
## Enable
Bundled plugins are disabled by default. Enable this one:
```bash
openclaw plugins enable copilot-proxy
```
Restart the Gateway after enabling.
## Authenticate
```bash
openclaw models auth login --provider copilot-proxy --set-default
```
## Notes
- Copilot Proxy must be running in VS Code.
- Base URL must include `/v1`.

View File

@ -1,142 +0,0 @@
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
const DEFAULT_API_KEY = "n/a";
const DEFAULT_CONTEXT_WINDOW = 128_000;
const DEFAULT_MAX_TOKENS = 8192;
const DEFAULT_MODEL_IDS = [
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5-mini",
"claude-opus-4.5",
"claude-sonnet-4.5",
"claude-haiku-4.5",
"gemini-3-pro",
"gemini-3-flash",
"grok-code-fast-1",
] as const;
function normalizeBaseUrl(value: string): string {
const trimmed = value.trim();
if (!trimmed) return DEFAULT_BASE_URL;
let normalized = trimmed;
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`;
return normalized;
}
function validateBaseUrl(value: string): string | undefined {
const normalized = normalizeBaseUrl(value);
try {
new URL(normalized);
} catch {
return "Enter a valid URL";
}
return undefined;
}
function parseModelIds(input: string): string[] {
const parsed = input
.split(/[\n,]/)
.map((model) => model.trim())
.filter(Boolean);
return Array.from(new Set(parsed));
}
function buildModelDefinition(modelId: string) {
return {
id: modelId,
name: modelId,
api: "openai-completions",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
};
}
const copilotProxyPlugin = {
id: "copilot-proxy",
name: "Copilot Proxy",
description: "Local Copilot Proxy (VS Code LM) provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api) {
api.registerProvider({
id: "copilot-proxy",
label: "Copilot Proxy",
docsPath: "/providers/models",
auth: [
{
id: "local",
label: "Local proxy",
hint: "Configure base URL + models for the Copilot Proxy server",
kind: "custom",
run: async (ctx) => {
const baseUrlInput = await ctx.prompter.text({
message: "Copilot Proxy base URL",
initialValue: DEFAULT_BASE_URL,
validate: validateBaseUrl,
});
const modelInput = await ctx.prompter.text({
message: "Model IDs (comma-separated)",
initialValue: DEFAULT_MODEL_IDS.join(", "),
validate: (value) =>
parseModelIds(value).length > 0 ? undefined : "Enter at least one model id",
});
const baseUrl = normalizeBaseUrl(baseUrlInput);
const modelIds = parseModelIds(modelInput);
const defaultModelId = modelIds[0] ?? DEFAULT_MODEL_IDS[0];
const defaultModelRef = `copilot-proxy/${defaultModelId}`;
return {
profiles: [
{
profileId: "copilot-proxy:local",
credential: {
type: "token",
provider: "copilot-proxy",
token: DEFAULT_API_KEY,
},
},
],
configPatch: {
models: {
providers: {
"copilot-proxy": {
baseUrl,
apiKey: DEFAULT_API_KEY,
api: "openai-completions",
authHeader: false,
models: modelIds.map((modelId) => buildModelDefinition(modelId)),
},
},
},
agents: {
defaults: {
models: Object.fromEntries(
modelIds.map((modelId) => [`copilot-proxy/${modelId}`, {}]),
),
},
},
},
defaultModel: defaultModelRef,
notes: [
"Start the Copilot Proxy VS Code extension before using these models.",
"Copilot Proxy serves /v1/chat/completions; base URL must include /v1.",
"Model availability depends on your Copilot plan; edit models.providers.copilot-proxy if needed.",
],
};
},
},
],
});
},
};
export default copilotProxyPlugin;

View File

@ -1,11 +0,0 @@
{
"id": "copilot-proxy",
"providers": [
"copilot-proxy"
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@ -1,11 +0,0 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.1.29",
"type": "module",
"description": "OpenClaw Copilot Proxy provider plugin",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@ -156,21 +156,19 @@ export function resolveDefaultConfigCandidates(
): string[] {
const explicit = env.OPENCLAW_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
if (explicit) return [resolveUserPath(explicit)];
const candidates: string[] = [];
// By default only prefer the canonical ~/.openclaw/openclaw.json candidate.
// When an explicit state dir override is supplied, include legacy filenames
// for that override so existing installs continue to work.
const openclawStateDir = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
if (openclawStateDir) {
const resolved = resolveUserPath(openclawStateDir);
candidates.push(path.join(resolved, CONFIG_FILENAME));
candidates.push(...LEGACY_CONFIG_FILENAMES.map((name) => path.join(resolved, name)));
return [
path.join(resolved, CONFIG_FILENAME),
...LEGACY_CONFIG_FILENAMES.map((name) => path.join(resolved, name)),
];
}
const defaultDirs = [newStateDir(homedir), ...legacyStateDirs(homedir)];
for (const dir of defaultDirs) {
candidates.push(path.join(dir, CONFIG_FILENAME));
candidates.push(...LEGACY_CONFIG_FILENAMES.map((name) => path.join(dir, name)));
}
return candidates;
return [path.join(newStateDir(homedir), CONFIG_FILENAME)];
}
export const DEFAULT_GATEWAY_PORT = 18789;

View File

@ -5,15 +5,7 @@ const DEFAULT_CONTEXT_WINDOW = 128_000;
const DEFAULT_MAX_TOKENS = 8192;
// Fallback model ids if SDK model discovery fails
const FALLBACK_MODEL_IDS = [
"gpt-4o",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"o1",
"o1-mini",
"o3-mini",
] as const;
const FALLBACK_MODEL_IDS = ["gpt-4o", "gpt-4.1", "gpt-5-mini", "grok-code-fast-1"] as const;
/**
* Get available model IDs from the Copilot SDK.

View File

@ -0,0 +1,75 @@
import path from "node:path";
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
import { resolveStateDir } from "../config/paths.js";
export function deriveCopilotApiBaseUrlFromToken(token: string): string {
const m = /proxy-ep=([^;]+)/.exec(token || "");
if (!m) return "https://api.github.com";
let ep = m[1];
// ensure we have a URL-like string
let proto = "https:";
let host = ep;
try {
if (/^https?:\/\//i.test(ep)) {
const u = new URL(ep);
proto = u.protocol;
host = u.hostname;
}
} catch {
// leave as-is
}
const parts = host.split(".").filter(Boolean);
if (parts.length === 0) return `${proto}//${host}`;
// replace first label with `api`
parts[0] = "api";
return `${proto}//${parts.join(".")}`;
}
type ResolveOptions = { githubToken: string; fetchImpl: typeof fetch };
export async function resolveCopilotApiToken(opts: ResolveOptions) {
const stateDir = resolveStateDir();
const cachePath = path.join(stateDir, "github-copilot-token.json");
const now = Date.now();
try {
const cached = await loadJsonFile(cachePath);
if (cached && typeof cached.expiresAt === "number" && cached.expiresAt > now) {
return {
token: cached.token,
baseUrl: deriveCopilotApiBaseUrlFromToken(cached.token),
source: `cache:${cached.updatedAt ?? "unknown"}`,
};
}
} catch {
// ignore cache read errors
}
const resp = await opts.fetchImpl("https://api.github.com/copilot/api_tokens", {
method: "POST",
headers: { Authorization: `Bearer ${opts.githubToken}`, Accept: "application/json" },
});
if (!resp || !resp.ok) {
throw new Error(`failed to fetch copilot token: ${resp?.status}`);
}
const body = await resp.json();
const token = String(body.token || "");
const expires_at = Number(
body.expires_at || body.expiresAt || Math.floor(Date.now() / 1000) + 3600,
);
const expiresAt = expires_at * 1000;
try {
await saveJsonFile(cachePath, { token, expiresAt, updatedAt: Date.now() });
} catch {
// ignore save errors
}
return {
token,
baseUrl: deriveCopilotApiBaseUrlFromToken(token),
source: "fetched",
};
}
export default { deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken };