From 1f7e1ca5d5997e6dc733c51cd74742a70335c208 Mon Sep 17 00:00:00 2001 From: Aditya Mer Date: Fri, 30 Jan 2026 12:11:59 +0530 Subject: [PATCH] chore: remove deprecated copilot-proxy extension; add github-copilot-token helper; adjust default config candidates; update docs --- docs/plugin.md | 2 +- docs/providers/github-copilot.md | 20 +-- extensions/copilot-proxy/README.md | 24 --- extensions/copilot-proxy/index.ts | 142 ------------------ extensions/copilot-proxy/openclaw.plugin.json | 11 -- extensions/copilot-proxy/package.json | 11 -- src/config/paths.ts | 18 +-- src/providers/github-copilot-models.ts | 10 +- src/providers/github-copilot-token.ts | 75 +++++++++ 9 files changed, 89 insertions(+), 224 deletions(-) delete mode 100644 extensions/copilot-proxy/README.md delete mode 100644 extensions/copilot-proxy/index.ts delete mode 100644 extensions/copilot-proxy/openclaw.plugin.json delete mode 100644 extensions/copilot-proxy/package.json create mode 100644 src/providers/github-copilot-token.ts diff --git a/docs/plugin.md b/docs/plugin.md index 383eedc49..207688666 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -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 diff --git a/docs/providers/github-copilot.md b/docs/providers/github-copilot.md index 50480aaf4..fc39cd392 100644 --- a/docs/providers/github-copilot.md +++ b/docs/providers/github-copilot.md @@ -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 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. +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 diff --git a/extensions/copilot-proxy/README.md b/extensions/copilot-proxy/README.md deleted file mode 100644 index 07ffde465..000000000 --- a/extensions/copilot-proxy/README.md +++ /dev/null @@ -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`. diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts deleted file mode 100644 index 0529b7bc5..000000000 --- a/extensions/copilot-proxy/index.ts +++ /dev/null @@ -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; diff --git a/extensions/copilot-proxy/openclaw.plugin.json b/extensions/copilot-proxy/openclaw.plugin.json deleted file mode 100644 index c27a03f7d..000000000 --- a/extensions/copilot-proxy/openclaw.plugin.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "copilot-proxy", - "providers": [ - "copilot-proxy" - ], - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": {} - } -} diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json deleted file mode 100644 index 60ca9dbbd..000000000 --- a/extensions/copilot-proxy/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@openclaw/copilot-proxy", - "version": "2026.1.29", - "type": "module", - "description": "OpenClaw Copilot Proxy provider plugin", - "openclaw": { - "extensions": [ - "./index.ts" - ] - } -} diff --git a/src/config/paths.ts b/src/config/paths.ts index 4f10c277f..4299889c5 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -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; diff --git a/src/providers/github-copilot-models.ts b/src/providers/github-copilot-models.ts index ae231818e..59da8706b 100644 --- a/src/providers/github-copilot-models.ts +++ b/src/providers/github-copilot-models.ts @@ -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. diff --git a/src/providers/github-copilot-token.ts b/src/providers/github-copilot-token.ts new file mode 100644 index 000000000..c3dff5daa --- /dev/null +++ b/src/providers/github-copilot-token.ts @@ -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 };