fix(github-copilot): mint tokens with vscode chat identity

This commit is contained in:
Andy Ye
2026-05-09 20:22:21 -07:00
committed by Peter Steinberger
parent 6d89bf65e0
commit c20450a82b
5 changed files with 45 additions and 6 deletions

View File

@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
- OpenAI/Codex: point gateway missing-key recovery and wizard docs at the canonical `openai/gpt-5.5` plus Codex OAuth route, and fix trajectory export errors so they suggest the valid `openclaw sessions` command.
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` primary, fallback, and model-map refs during config load and unrelated config writes so saved config keeps targeting Gemini 3.1 Pro Preview.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside emitted Google provider model config, so regenerated models.json rows test `google/gemini-3.1-pro-preview`.
- GitHub Copilot: mint short-lived Copilot API tokens with the same `vscode-chat` integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the `copilot-language-server` scope. Fixes #79946.
- Plugins/doctor: drop stale managed npm install records when `openclaw doctor --fix` removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.
- Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice.
- Discord/voice: synthesize realtime playback timestamps from emitted Discord PCM so OpenAI realtime barge-in truncation no longer sees `audioEndMs=0` and skips legitimate interruptions.

View File

@@ -359,6 +359,7 @@ describe("github-copilot token", () => {
token: "cached;proxy-ep=proxy.example.com;",
expiresAt: now + 60 * 60 * 1000,
updatedAt: now,
integrationId: "vscode-chat",
});
const fetchImpl = vi.fn();

View File

@@ -1,7 +1,7 @@
import type { Context } from "@mariozechner/pi-ai";
import { buildCopilotIdeHeaders } from "../plugin-sdk/provider-auth.js";
import { COPILOT_INTEGRATION_ID, buildCopilotIdeHeaders } from "../plugin-sdk/provider-auth.js";
export { buildCopilotIdeHeaders } from "../plugin-sdk/provider-auth.js";
export { COPILOT_INTEGRATION_ID, buildCopilotIdeHeaders } from "../plugin-sdk/provider-auth.js";
function inferCopilotInitiator(messages: Context["messages"]): "agent" | "user" {
const last = messages[messages.length - 1];
@@ -43,7 +43,7 @@ export function buildCopilotDynamicHeaders(params: {
}): Record<string, string> {
return {
...buildCopilotIdeHeaders(),
"Copilot-Integration-Id": "vscode-chat",
"Copilot-Integration-Id": COPILOT_INTEGRATION_ID,
"Openai-Organization": "github-copilot",
"x-initiator": inferCopilotInitiator(params.messages),
...(params.hasImages ? { "Copilot-Vision-Request": "true" } : {}),

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { buildCopilotIdeHeaders } from "./copilot-dynamic-headers.js";
import { COPILOT_INTEGRATION_ID, buildCopilotIdeHeaders } from "./copilot-dynamic-headers.js";
import {
deriveCopilotApiBaseUrlFromToken,
resolveCopilotApiToken,
@@ -47,7 +47,7 @@ describe("resolveCopilotApiToken", () => {
expect(result.expiresAt).toBe(12_345_678_901_000);
});
it("sends IDE headers when exchanging the GitHub token", async () => {
it("sends IDE and integration headers when exchanging the GitHub token", async () => {
const fetchImpl = vi.fn(async () => ({
ok: true,
json: async () => ({
@@ -71,7 +71,40 @@ describe("resolveCopilotApiToken", () => {
expect(init.headers).toEqual({
Accept: "application/json",
Authorization: "Bearer github-token",
"Copilot-Integration-Id": COPILOT_INTEGRATION_ID,
...buildCopilotIdeHeaders({ includeApiVersion: true }),
});
});
it("refreshes legacy cached tokens without the vscode-chat integration identity", async () => {
const fetchImpl = vi.fn(async () => ({
ok: true,
json: async () => ({
token: "fresh-copilot-token",
expires_at: Math.floor(Date.now() / 1000) + 3600,
}),
}));
const saveJsonFileImpl = vi.fn();
const result = await resolveCopilotApiToken({
githubToken: "github-token",
cachePath: "/tmp/github-copilot-token-test.json",
loadJsonFileImpl: () => ({
token: "legacy-copilot-token",
expiresAt: Date.now() + 60 * 60 * 1000,
updatedAt: Date.now(),
}),
saveJsonFileImpl,
fetchImpl: fetchImpl as unknown as typeof fetch,
});
expect(result.token).toBe("fresh-copilot-token");
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(saveJsonFileImpl).toHaveBeenCalledWith("/tmp/github-copilot-token-test.json", {
token: "fresh-copilot-token",
expiresAt: expect.any(Number),
updatedAt: expect.any(Number),
integrationId: COPILOT_INTEGRATION_ID,
});
});
});

View File

@@ -98,12 +98,14 @@ export const COPILOT_EDITOR_VERSION = "vscode/1.96.2";
export const COPILOT_USER_AGENT = "GitHubCopilotChat/0.26.7";
export const COPILOT_EDITOR_PLUGIN_VERSION = "copilot-chat/0.35.0";
export const COPILOT_GITHUB_API_VERSION = "2025-04-01";
export const COPILOT_INTEGRATION_ID = "vscode-chat";
export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
export type CachedCopilotToken = {
token: string;
expiresAt: number;
updatedAt: number;
integrationId?: string;
};
export function buildCopilotIdeHeaders(
@@ -124,7 +126,7 @@ function resolveCopilotTokenCachePath(env: NodeJS.ProcessEnv = process.env) {
}
function isCopilotTokenUsable(cache: CachedCopilotToken, now = Date.now()): boolean {
return cache.expiresAt - now > 5 * 60 * 1000;
return cache.integrationId === COPILOT_INTEGRATION_ID && cache.expiresAt - now > 5 * 60 * 1000;
}
function parseCopilotTokenResponse(value: unknown): {
@@ -232,6 +234,7 @@ export async function resolveCopilotApiToken(params: {
headers: {
Accept: "application/json",
Authorization: `Bearer ${params.githubToken}`,
"Copilot-Integration-Id": COPILOT_INTEGRATION_ID,
...buildCopilotIdeHeaders({ includeApiVersion: true }),
},
});
@@ -245,6 +248,7 @@ export async function resolveCopilotApiToken(params: {
token: json.token,
expiresAt: json.expiresAt,
updatedAt: Date.now(),
integrationId: COPILOT_INTEGRATION_ID,
};
saveJsonFileFn(cachePath, payload);