@earendil-works/pi-ai
Version:
Unified LLM API with automatic model discovery and provider configuration
385 lines • 15.4 kB
JavaScript
/**
* GitHub Copilot OAuth flow
*/
import { GITHUB_COPILOT_MODELS } from "../../providers/github-copilot.models.js";
import { sleep } from "../../utils/sleep.js";
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
import { trustedHttpUrl } from "./trusted-url.js";
const decode = (s) => atob(s);
const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
const COPILOT_HEADERS = {
"User-Agent": "GitHubCopilotChat/0.35.0",
"Editor-Version": "vscode/1.107.0",
"Editor-Plugin-Version": "copilot-chat/0.35.0",
"Copilot-Integration-Id": "vscode-chat",
};
const COPILOT_API_VERSION = "2026-06-01";
function normalizeDomain(input) {
const trimmed = input.trim();
if (!trimmed)
return null;
try {
const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
return url.hostname;
}
catch {
return null;
}
}
function getUrls(domain) {
return {
deviceCodeUrl: `https://${domain}/login/device/code`,
accessTokenUrl: `https://${domain}/login/oauth/access_token`,
copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,
};
}
/**
* Parse the proxy-ep from a Copilot token and convert to API base URL.
* Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
* Returns API URL like https://api.individual.githubcopilot.com
*/
function getBaseUrlFromToken(token) {
const match = token.match(/proxy-ep=([^;]+)/);
if (!match)
return null;
const proxyHost = match[1];
// Convert proxy.xxx to api.xxx
const apiHost = proxyHost.replace(/^proxy\./, "api.");
return `https://${apiHost}`;
}
function getGitHubCopilotBaseUrl(token, enterpriseDomain) {
// If we have a token, extract the base URL from proxy-ep
if (token) {
const urlFromToken = getBaseUrlFromToken(token);
if (urlFromToken)
return urlFromToken;
}
// Fallback for enterprise or if token parsing fails
if (enterpriseDomain)
return `https://copilot-api.${enterpriseDomain}`;
return "https://api.individual.githubcopilot.com";
}
function asRecord(value) {
return value && typeof value === "object" ? value : undefined;
}
function parseGitHubCopilotModelCatalog(raw, allowPolicyFallback) {
const data = asRecord(raw)?.data;
if (!Array.isArray(data)) {
throw new Error("Invalid Copilot models response");
}
const accountModels = data.flatMap((rawItem) => {
const item = asRecord(rawItem);
const id = item?.id;
if (!item || typeof id !== "string")
return [];
const capabilities = asRecord(item.capabilities);
const supports = asRecord(capabilities?.supports);
if (supports?.tool_calls === false)
return [];
return [
{
id,
pickerEnabled: item.model_picker_enabled === true,
policyState: asRecord(item.policy)?.state,
},
];
});
const pickerModelIds = accountModels
.filter((model) => model.pickerEnabled && model.policyState !== "disabled")
.map((model) => model.id);
const usePolicyFallback = allowPolicyFallback && pickerModelIds.length === 0;
const availableModelIds = pickerModelIds.length > 0 || !allowPolicyFallback
? pickerModelIds
: accountModels.filter((model) => model.policyState === "enabled").map((model) => model.id);
const policyModelIds = accountModels
.filter((model) => model.policyState === "unconfigured" &&
Object.hasOwn(GITHUB_COPILOT_MODELS, model.id) &&
(model.pickerEnabled || usePolicyFallback))
.map((model) => model.id);
return { availableModelIds, policyModelIds };
}
async function fetchWithRateLimitRetry(url, init, signal, retryPolicy) {
const retryBudgetSignal = retryPolicy.maxRetries > 0 && retryPolicy.maxElapsedMs > 0
? AbortSignal.timeout(retryPolicy.maxElapsedMs)
: undefined;
const requestSignal = retryBudgetSignal ? AbortSignal.any([signal, retryBudgetSignal]) : signal;
const retryDeadline = retryBudgetSignal ? Date.now() + retryPolicy.maxElapsedMs : undefined;
for (let retry = 0;; retry++) {
const response = await fetch(url, {
...init,
signal: AbortSignal.any([requestSignal, AbortSignal.timeout(5000)]),
});
if (response.status !== 429 || retry === retryPolicy.maxRetries)
return response;
const retryAfter = response.headers.get("retry-after");
let delayMs = 500 * 2 ** retry;
if (retryAfter) {
const seconds = Number.parseFloat(retryAfter);
delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
if (!Number.isFinite(delayMs))
return response;
}
delayMs = Math.max(0, delayMs);
if (retryDeadline !== undefined && delayMs >= retryDeadline - Date.now())
return response;
await response.body?.cancel();
await sleep(delayMs, requestSignal);
}
}
async function fetchGitHubCopilotModels(copilotToken, enterpriseDomain, signal, retryPolicy) {
const baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain);
// Some Individual accounts return false for every picker flag despite explicit enabled policies.
// Limit the fallback to that endpoint so other account types keep strict picker semantics.
const allowPolicyFallback = baseUrl === "https://api.individual.githubcopilot.com";
const response = await fetchWithRateLimitRetry(`${baseUrl}/models`, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${copilotToken}`,
...COPILOT_HEADERS,
"X-GitHub-Api-Version": COPILOT_API_VERSION,
},
}, signal, retryPolicy);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
}
return parseGitHubCopilotModelCatalog(await response.json(), allowPolicyFallback);
}
async function fetchJson(url, init) {
const response = await fetch(url, init);
if (!response.ok) {
const text = await response.text();
throw new Error(`${response.status} ${response.statusText}: ${text}`);
}
return response.json();
}
async function startDeviceFlow(domain, signal) {
const urls = getUrls(domain);
const data = await fetchJson(urls.deviceCodeUrl, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "GitHubCopilotChat/0.35.0",
},
body: new URLSearchParams({
client_id: CLIENT_ID,
scope: "read:user",
}),
signal,
});
if (!data || typeof data !== "object") {
throw new Error("Invalid device code response");
}
const deviceCode = data.device_code;
const userCode = data.user_code;
const verificationUri = data.verification_uri;
const interval = data.interval;
const expiresIn = data.expires_in;
if (typeof deviceCode !== "string" ||
typeof userCode !== "string" ||
typeof verificationUri !== "string" ||
(interval !== undefined && typeof interval !== "number") ||
typeof expiresIn !== "number") {
throw new Error("Invalid device code response fields");
}
const trustedVerificationUri = trustedHttpUrl(verificationUri);
if (!trustedVerificationUri) {
throw new Error("Untrusted verification_uri in device code response");
}
return {
device_code: deviceCode,
user_code: userCode,
verification_uri: trustedVerificationUri,
interval,
expires_in: expiresIn,
};
}
async function pollForGitHubAccessToken(domain, device, signal) {
const urls = getUrls(domain);
return pollOAuthDeviceCodeFlow({
intervalSeconds: device.interval,
expiresInSeconds: device.expires_in,
waitBeforeFirstPoll: true,
signal,
poll: async () => {
const raw = await fetchJson(urls.accessTokenUrl, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "GitHubCopilotChat/0.35.0",
},
body: new URLSearchParams({
client_id: CLIENT_ID,
device_code: device.device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
signal,
});
if (raw && typeof raw === "object" && typeof raw.access_token === "string") {
return { status: "complete", value: raw.access_token };
}
if (raw && typeof raw === "object" && typeof raw.error === "string") {
const { error, error_description: description, interval } = raw;
if (error === "authorization_pending") {
return { status: "pending" };
}
if (error === "slow_down") {
return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
}
const descriptionSuffix = description ? `: ${description}` : "";
return { status: "failed", message: `Device flow failed: ${error}${descriptionSuffix}` };
}
return { status: "failed", message: "Invalid device token response" };
},
});
}
async function refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain, signal) {
const domain = enterpriseDomain || "github.com";
const urls = getUrls(domain);
const raw = await fetchJson(urls.copilotTokenUrl, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${refreshToken}`,
...COPILOT_HEADERS,
},
signal,
});
if (!raw || typeof raw !== "object") {
throw new Error("Invalid Copilot token response");
}
const token = raw.token;
const expiresAt = raw.expires_at;
if (typeof token !== "string" || typeof expiresAt !== "number") {
throw new Error("Invalid Copilot token response fields");
}
return {
type: "oauth",
refresh: refreshToken,
access: token,
expires: expiresAt * 1000 - 5 * 60 * 1000,
enterpriseUrl: enterpriseDomain,
};
}
/**
* Refresh GitHub Copilot token
*/
async function refreshGitHubCopilotToken(refreshToken, enterpriseDomain, signal) {
const credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain, signal);
const { availableModelIds } = await fetchGitHubCopilotModels(credentials.access, enterpriseDomain, signal, {
maxRetries: 0,
maxElapsedMs: 0,
});
return {
...credentials,
availableModelIds,
};
}
/**
* Enable a model for the user's GitHub Copilot account.
* This is required for some models (like Claude, Grok) before they can be used.
*/
async function enableGitHubCopilotModel(token, modelId, enterpriseDomain, signal) {
const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
const url = `${baseUrl}/models/${modelId}/policy`;
let response;
try {
response = await fetchWithRateLimitRetry(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...COPILOT_HEADERS,
"openai-intent": "chat-policy",
"x-interaction-type": "chat-policy",
},
body: JSON.stringify({ state: "enabled" }),
}, signal, { maxRetries: 2, maxElapsedMs: 5000 });
}
catch (error) {
if (signal.aborted)
throw error;
return false;
}
if (response.status === 429) {
throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
}
return response.ok;
}
/**
* Enable the requested GitHub Copilot models and return the successful IDs.
* Policy updates are best effort; exhausted rate limiting stops the batch.
*/
async function enableGitHubCopilotModels(token, modelIds, enterpriseDomain, signal) {
const enabledModelIds = [];
for (const modelId of modelIds) {
try {
if (await enableGitHubCopilotModel(token, modelId, enterpriseDomain, signal)) {
enabledModelIds.push(modelId);
}
}
catch (error) {
if (signal.aborted)
throw error;
break;
}
}
return enabledModelIds;
}
async function loginGitHubCopilot(interaction) {
const input = await interaction.prompt({
type: "text",
message: "GitHub Enterprise URL/domain (blank for github.com)",
placeholder: "company.ghe.com",
});
if (interaction.signal.aborted)
throw new Error("Login cancelled");
const trimmed = input.trim();
const enterpriseDomain = normalizeDomain(input);
if (trimmed && !enterpriseDomain)
throw new Error("Invalid GitHub Enterprise URL/domain");
const domain = enterpriseDomain || "github.com";
const device = await startDeviceFlow(domain, interaction.signal);
interaction.notify({
type: "device_code",
userCode: device.user_code,
verificationUri: device.verification_uri,
intervalSeconds: device.interval,
expiresInSeconds: device.expires_in,
});
const githubAccessToken = await pollForGitHubAccessToken(domain, device, interaction.signal);
const credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined, interaction.signal);
const models = await fetchGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined, interaction.signal, {
maxRetries: 2,
maxElapsedMs: 5000,
});
let enabledModelIds = [];
if (models.policyModelIds.length > 0) {
interaction.notify({ type: "progress", message: "Enabling models..." });
enabledModelIds = await enableGitHubCopilotModels(credentials.access, models.policyModelIds, enterpriseDomain ?? undefined, interaction.signal);
}
return {
...credentials,
availableModelIds: [...new Set([...models.availableModelIds, ...enabledModelIds])],
};
}
function copilotEnterpriseDomain(credential) {
const enterpriseUrl = credential.enterpriseUrl;
if (typeof enterpriseUrl !== "string" || !enterpriseUrl)
return undefined;
return normalizeDomain(enterpriseUrl) ?? undefined;
}
export const githubCopilotOAuth = {
name: "GitHub Copilot",
isSubscription: true,
login: loginGitHubCopilot,
refresh: (credential, signal) => refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential), signal),
/** Derive the credential-specific proxy endpoint for each request. */
async toAuth(credential) {
return {
apiKey: credential.access,
baseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),
};
},
};
//# sourceMappingURL=github-copilot.js.map