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

UnusualNick commented Feb 4, 2026

Is there a way to specify the reasoning setting

Well, there are two ways. You can try to add corresponding headers to the request (ones, specified in the openai api spec) or can just try copilot cli, newly made by GitHub, I think they have such preference configurable

@kldzj
Copy link

kldzj commented Feb 5, 2026

I received a slow_down error and just added another delay of 5s before the while loop (await new Promise((resolve) => setTimeout(resolve, 5000));) in copilot-initial-auth.js, then it worked fine

@dpearson2699
Copy link

dpearson2699 commented Feb 6, 2026

Update: I've forked this gist with full GPT Codex support and xhigh reasoning built in:

πŸ‘‰ https://gist.github.com/dpearson2699/d7e797a85b4286a822dcb9d00f2bebe8

What's different from the original

GPT Codex support (Responses API) β€” Codex models (gpt-5.2-codex, gpt-5.1-codex, etc.) only work with OpenAI's Responses API, not /chat/completions. The updated copilot-transformer.js automatically:

  • Detects Codex models and reroutes from /chat/completions β†’ /responses
  • Converts the request body format (messages β†’ input, tool format changes, system β†’ instructions)
  • Translates Responses API streaming events back into Chat Completions chunks so claude-code-router can consume them

xhigh reasoning β€” Automatically injects reasoning_effort: "xhigh" for GPT-5.2, GPT-5.3, and GPT-5.1-codex-max models. For Codex models this is mapped to the Responses API reasoning.effort field.

Config gotcha β€” Do NOT add model-specific transformer entries in config.json (e.g. "gpt-5.2-codex": { "use": [...] }). The framework's model-specific chain doesn't properly unwrap the { body, config } return format, causing requests to break with "model not supported" / "messages must be non-empty" errors. Only use the provider-level "use" array.


Original comment for reference:

Per the OpenAI API spec, reasoning_effort is a standard body parameter with supported values: none, minimal, low, medium, high, xhigh. Note that xhigh is only supported for models after gpt-5.1-codex-max (i.e., gpt-5.2, gpt-5.2-codex, gpt-5.3, etc.).

Credit to opencode's implementation for the reference: https://github.com/sst/opencode/blob/dev/packages/opencode/src/provider/transform.ts

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