Skip to content

Instantly share code, notes, and snippets.

@joshbalfour
Created January 6, 2026 11:12
Show Gist options
  • Select an option

  • Save joshbalfour/d5485c5548c4f839900520be7d36be3c to your computer and use it in GitHub Desktop.

Select an option

Save joshbalfour/d5485c5548c4f839900520be7d36be3c to your computer and use it in GitHub Desktop.

Usage

Before starting claude code router, run node .claude-code-router/copilot-initial-auth.js

Tokens will refresh in the background.

Notes

  • Store tokens in a file, by default in your home directory, overridable by $COPILOT_TOKEN_FILE env var.
  • Seems to work with any kind of github account (tested with personal/business) since the completions endpoint is contained in the copilot token
{
"Providers": [
{
"name": "copilot",
"api_base_url": "populated-by-transformer",
"api_key": "populated-by-transformer",
"models": [
"claude-sonnet-4.5"
],
"transformer": {
"use": [
"copilot-transformer"
]
}
}
],
"transformers": [
{
"path": "/root/.claude-code-router/copilot-transformer.js"
}
],
"Router": {
"default": "copilot,claude-sonnet-4.5"
}
}
// /home/user/.claude-code-router/auth/github-copilot.js
const fs = require("fs");
const path = require("path");
class GitHubCopilotAuth {
constructor() {
this.CLIENT_ID = "01ab8ac9400c4e429b23";
this.DEVICE_CODE_URL = "https://github.com/login/device/code";
this.ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
this.COPILOT_API_KEY_URL =
"https://api.github.com/copilot_internal/v2/token";
this.TOKEN_FILE_PATH = process.env.COPILOT_TOKEN_FILE || path.join(
process.env.HOME || process.env.USERPROFILE,
".copilot-tokens.json"
);
}
async startDeviceFlow() {
const response = await fetch(this.DEVICE_CODE_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: this.CLIENT_ID,
scope: "read:user",
}),
});
const data = await response.json();
return {
deviceCode: data.device_code,
userCode: data.user_code,
verificationUri: data.verification_uri,
interval: data.interval || 5,
expiresIn: data.expires_in,
};
}
async pollForToken(deviceCode) {
const response = await fetch(this.ACCESS_TOKEN_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: this.CLIENT_ID,
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
});
const data = await response.json();
if (data.access_token) {
return { success: true, accessToken: data.access_token };
}
if (data.error === "authorization_pending") {
return { pending: true };
}
return { error: data.error };
}
async getCopilotToken(githubAccessToken) {
const response = await fetch(this.COPILOT_API_KEY_URL, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${githubAccessToken}`,
"User-Agent": "GitHubCopilotChat/0.26.7",
"Editor-Version": "vscode/1.99.3",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
},
});
if (!response.ok) {
throw new Error(`Failed to get Copilot token: ${response.statusText}`);
}
const tokenData = await response.json();
return {
token: tokenData.token,
expiresAt: tokenData.expires_at,
endpoint:
tokenData.endpoints?.api ||
"https://copilot-proxy.githubusercontent.com/v1/engines/copilot-codex/completions",
};
}
isTokenExpired(bufferMinutes = 5) {
try {
const tokenFile = this.TOKEN_FILE_PATH;
if (!fs.existsSync(tokenFile)) {
return true;
}
const data = JSON.parse(fs.readFileSync(tokenFile, "utf8"));
if (!data.expiresAt) {
return true;
}
const now = Math.floor(Date.now() / 1000);
const bufferTime = bufferMinutes * 60;
return now >= data.expiresAt - bufferTime;
} catch (error) {
console.error("Error checking token expiration:", error);
return true;
}
}
getTokenFromFile() {
const tokenFile = this.TOKEN_FILE_PATH;
if (!fs.existsSync(tokenFile)) {
return;
}
const data = JSON.parse(fs.readFileSync(tokenFile, "utf8"));
return data;
}
updateTokenFile(tokenData) {
try {
const tokenFile = this.TOKEN_FILE_PATH;
fs.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2));
} catch (error) {
console.error("Error updating token files:", error);
}
}
async refreshCopilotToken() {
try {
const existingData = this.getTokenFromFile();
if (!existingData.githubToken) {
throw new Error("No GitHub token found. Please re-authenticate.");
}
console.log("Refreshing Copilot token...");
const copilotToken = await this.getCopilotToken(existingData.githubToken);
const tokenData = {
githubToken: existingData.githubToken,
copilotToken: copilotToken.token,
endpoint: `${copilotToken.endpoint}/chat/completions`,
expiresAt: copilotToken.expiresAt,
lastUpdated: new Date().toISOString(),
};
this.updateTokenFile(tokenData);
console.log("Copilot token refreshed successfully!");
return tokenData;
} catch (error) {
throw new Error(`Failed to refresh Copilot token: ${error.message}`);
}
}
}
module.exports = GitHubCopilotAuth;
const GitHubCopilotAuth = require("./copilot-auth");
const auth = new GitHubCopilotAuth();
async function setupCopilotAuth() {
console.log("Setting up GitHub Copilot authentication...\n");
const deviceFlow = await auth.startDeviceFlow();
console.log("📱 Please visit:", deviceFlow.verificationUri);
console.log("🔑 Enter this code:", deviceFlow.userCode);
console.log("\nWaiting for authorization...\n");
let attempts = 0;
const maxAttempts = deviceFlow.expiresIn / deviceFlow.interval;
while (attempts < maxAttempts) {
await new Promise((resolve) =>
setTimeout(resolve, deviceFlow.interval * 1000)
);
const result = await auth.pollForToken(deviceFlow.deviceCode);
if (result.success) {
console.log("GitHub OAuth successful!");
console.log("Getting Copilot session token...");
const copilotToken = await auth.getCopilotToken(result.accessToken);
const tokenData = {
githubToken: result.accessToken,
copilotToken: copilotToken.token,
endpoint: `${copilotToken.endpoint}/chat/completions`,
expiresAt: copilotToken.expiresAt,
lastUpdated: new Date().toISOString(),
};
auth.updateTokenFile(tokenData);
console.log("🔧 Token file updated automatically!");
console.log("Setup complete! Tokens saved.");
return;
}
if (result.error) {
console.error("Authentication failed:", result.error);
return;
}
if (!auth.isTokenExpired()) {
console.log("Token file is valid now. Exiting wait loop.");
}
attempts++;
process.stdout.write("⏳ ");
}
console.log("\n❌ Authentication timed out. Please try again.");
}
(async () => {
try {
if (!auth.isTokenExpired()) {
console.log("Existing Copilot token is still valid. No action needed.");
return;
} else {
// Try to refresh if possible
try {
await auth.refreshCopilotToken();
console.log("Copilot token refreshed.");
return;
} catch (refreshErr) {
console.log("Refresh failed or no credentials. Starting device authentication...");
}
await setupCopilotAuth();
}
} catch (err) {
console.error("Unexpected error during Copilot authentication:", err);
process.exit(1);
}
})()
// /home/user/.claude-code-router/plugins/copilot.js
const crypto = require("crypto");
const GitHubCopilotAuth = require("./copilot-auth.js");
class CopilotTransformer {
name = "copilot-transformer";
constructor() {
this.auth = new GitHubCopilotAuth();
this.VERSION = "0.26.7";
this.EDITOR_VERSION = "vscode/1.103.2";
this.API_VERSION = "2025-04-01";
}
loadToken() {
return this.auth.getTokenFromFile()
}
copilotHeaders({ vision, isAgent }) {
const headers = {
"Copilot-Integration-ID": "vscode-chat",
"Editor-Plugin-Version": `copilot-chat/${this.VERSION}`,
"Editor-Version": this.EDITOR_VERSION,
"User-Agent": `GitHubCopilotChat/${this.VERSION}`,
"OpenAI-Intent": "conversation-panel",
"x-github-api-version": this.API_VERSION,
"X-Initiator": isAgent ? "agent" : "user",
"x-request-id": crypto.randomUUID(),
"x-vscode-user-agent-library-version": "electron-fetch",
"Content-Type": "application/json",
};
if (vision) {
headers["copilot-vision-request"] = "true";
}
return headers;
}
async transformRequestIn(request) {
if (this.auth.isTokenExpired()) {
try {
await this.auth.refreshCopilotToken();
} catch (error) {
throw new Error(
`Token refresh failed: ${error.message}.`
);
}
}
let tokenData = this.loadToken();
const messages = request.messages || [];
const vision = messages.some(
(m) =>
typeof m.content !== "string" &&
Array.isArray(m.content) &&
m.content.some((c) => c.type === "image_url")
);
const isAgent = messages.some((m) =>
["assistant", "tool"].includes(m.role)
);
const headers = this.copilotHeaders({ vision, isAgent });
headers.Authorization = `Bearer ${tokenData.copilotToken}`;
return {
body: {
...request,
model: request.model?.split(",").pop() || request.model,
},
config: {
url: tokenData.endpoint,
headers,
},
};
}
async transformResponseOut(response) {
return response;
}
}
module.exports = CopilotTransformer;
@UnusualNick
Copy link

There is a minor inconvenience, don't you have path of copilot-initial-auth.js mixed up in a readme? It just imports files through "./", meaning they should be in the same directory(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment