|
// /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; |
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 updatedcopilot-transformer.jsautomatically:/chat/completionsβ/responsesxhigh 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 APIreasoning.effortfield.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_effortis a standard body parameter with supported values:none,minimal,low,medium,high,xhigh. Note thatxhighis 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