Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379import { COPILOT_CLIENT_HEADERS, COPILOT_CLIENT_USER_AGENT } from "../../copilot-client-version.js";import { getModels } from "../../models.js";import type { Api, Model } from "../../types.js";import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js";
type CopilotCredentials = OAuthCredentials & { enterpriseUrl?: string;};
const decode = (s: string) => atob(s);const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
const COPILOT_HEADERS = COPILOT_CLIENT_HEADERS;
const INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2;const SLOW_DOWN_POLL_INTERVAL_MULTIPLIER = 1.4;
type DeviceCodeResponse = { device_code: string; user_code: string; verification_uri: string; interval: number; expires_in: number;};
type DeviceTokenSuccessResponse = { access_token: string; token_type?: string; scope?: string;};
type DeviceTokenErrorResponse = { error: string; error_description?: string; interval?: number;};
export function normalizeDomain(input: string): string | null { 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: string): { deviceCodeUrl: string; accessTokenUrl: string; copilotTokenUrl: string;} { 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: string): string | null { const match = token.match(/proxy-ep=([^;]+)/); if (!match) return null; const proxyHost = match[1]; const apiHost = proxyHost.replace(/^proxy\./, "api."); return `https://${apiHost}`;}
export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string { if (token) { const urlFromToken = getBaseUrlFromToken(token); if (urlFromToken) return urlFromToken; } if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`; return "https://api.individual.githubcopilot.com";}
async function fetchJson(url: string, init: RequestInit): Promise<unknown> { 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: string): Promise<DeviceCodeResponse> { 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": COPILOT_CLIENT_USER_AGENT, }, body: new URLSearchParams({ client_id: CLIENT_ID, scope: "read:user", }), });
if (!data || typeof data !== "object") { throw new Error("Invalid device code response"); }
const deviceCode = (data as Record<string, unknown>).device_code; const userCode = (data as Record<string, unknown>).user_code; const verificationUri = (data as Record<string, unknown>).verification_uri; const interval = (data as Record<string, unknown>).interval; const expiresIn = (data as Record<string, unknown>).expires_in;
if ( typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string" || typeof interval !== "number" || typeof expiresIn !== "number" ) { throw new Error("Invalid device code response fields"); }
return { device_code: deviceCode, user_code: userCode, verification_uri: verificationUri, interval, expires_in: expiresIn, };}
function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new Error("Login cancelled")); return; }
const timeout = setTimeout(resolve, ms);
signal?.addEventListener( "abort", () => { clearTimeout(timeout); reject(new Error("Login cancelled")); }, { once: true }, ); });}
async function pollForGitHubAccessToken( domain: string, deviceCode: string, intervalSeconds: number, expiresIn: number, signal?: AbortSignal,) { const urls = getUrls(domain); const deadline = Date.now() + expiresIn * 1000; let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000)); let intervalMultiplier = INITIAL_POLL_INTERVAL_MULTIPLIER; let slowDownResponses = 0;
while (Date.now() < deadline) { if (signal?.aborted) { throw new Error("Login cancelled"); }
const remainingMs = deadline - Date.now(); const waitMs = Math.min(Math.ceil(intervalMs * intervalMultiplier), remainingMs); await abortableSleep(waitMs, signal);
const raw = await fetchJson(urls.accessTokenUrl, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": COPILOT_CLIENT_USER_AGENT, }, body: new URLSearchParams({ client_id: CLIENT_ID, device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), });
if (raw && typeof raw === "object" && typeof (raw as DeviceTokenSuccessResponse).access_token === "string") { return (raw as DeviceTokenSuccessResponse).access_token; }
if (raw && typeof raw === "object" && typeof (raw as DeviceTokenErrorResponse).error === "string") { const { error, error_description: description, interval } = raw as DeviceTokenErrorResponse; if (error === "authorization_pending") { continue; }
if (error === "slow_down") { slowDownResponses += 1; intervalMs = typeof interval === "number" && interval > 0 ? interval * 1000 : Math.max(1000, intervalMs + 5000); intervalMultiplier = SLOW_DOWN_POLL_INTERVAL_MULTIPLIER; continue; }
const descriptionSuffix = description ? `: ${description}` : ""; throw new Error(`Device flow failed: ${error}${descriptionSuffix}`); } }
if (slowDownResponses > 0) { throw new Error( "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.", ); }
throw new Error("Device flow timed out");}
export async function refreshGitHubCopilotToken( refreshToken: string, enterpriseDomain?: string,): Promise<OAuthCredentials> { const domain = enterpriseDomain || "github.com"; const urls = getUrls(domain);
const raw = await fetchJson(urls.copilotTokenUrl, { headers: { Accept: "application/json", Authorization: `Bearer ${refreshToken}`, ...COPILOT_HEADERS, }, });
if (!raw || typeof raw !== "object") { throw new Error("Invalid Copilot token response"); }
const token = (raw as Record<string, unknown>).token; const expiresAt = (raw as Record<string, unknown>).expires_at;
if (typeof token !== "string" || typeof expiresAt !== "number") { throw new Error("Invalid Copilot token response fields"); }
return { refresh: refreshToken, access: token, expires: expiresAt * 1000 - 5 * 60 * 1000, enterpriseUrl: enterpriseDomain, };}
/** * 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: string, modelId: string, enterpriseDomain?: string): Promise<boolean> { const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain); const url = `${baseUrl}/models/${modelId}/policy`;
try { const response = await fetch(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" }), }); return response.ok; } catch { return false; }}
/** * Enable all known GitHub Copilot models that may require policy acceptance. * Called after successful login to ensure all models are available. */async function enableAllGitHubCopilotModels( token: string, enterpriseDomain?: string, onProgress?: (model: string, success: boolean) => void,): Promise<void> { const models = getModels("github-copilot"); await Promise.all( models.map(async (model) => { const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain); onProgress?.(model.id, success); }), );}
/** * Login with GitHub Copilot OAuth (device code flow) * * @param options.onAuth - Callback with URL and optional instructions (user code) * @param options.onPrompt - Callback to prompt user for input * @param options.onProgress - Optional progress callback * @param options.signal - Optional AbortSignal for cancellation */export async function loginGitHubCopilot(options: { onAuth: (url: string, instructions?: string) => void; onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>; onProgress?: (message: string) => void; signal?: AbortSignal;}): Promise<OAuthCredentials> { const input = await options.onPrompt({ message: "GitHub Enterprise URL/domain (blank for github.com)", placeholder: "company.ghe.com", allowEmpty: true, });
if (options.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); options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
const githubAccessToken = await pollForGitHubAccessToken( domain, device.device_code, device.interval, device.expires_in, options.signal, ); const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);
options.onProgress?.("Enabling models..."); await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined); return credentials;}
export const githubCopilotOAuthProvider: OAuthProviderInterface = { id: "github-copilot", name: "GitHub Copilot",
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> { return loginGitHubCopilot({ onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }), onPrompt: callbacks.onPrompt, onProgress: callbacks.onProgress, signal: callbacks.signal, }); },
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> { const creds = credentials as CopilotCredentials; return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl); },
getApiKey(credentials: OAuthCredentials): string { return credentials.access; },
modifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] { const creds = credentials as CopilotCredentials; const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined; const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain); return models.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m)); },};