mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 06:02:42 +00:00
Add GitHub Copilot support (#191)
- OAuth login for GitHub Copilot via /login command - Support for github.com and GitHub Enterprise - Models sourced from models.dev (Claude, GPT, Gemini, Grok, etc.) - Dynamic base URL from token's proxy-ep field - Use vscode-chat integration ID for API compatibility - Documentation for model enablement at github.com/settings/copilot/features Co-authored-by: cau1k <cau1k@users.noreply.github.com>
This commit is contained in:
parent
ce4ba70d33
commit
b66157c649
7 changed files with 664 additions and 726 deletions
|
|
@ -242,18 +242,15 @@ export function loadAndMergeModels(): { models: Model<Api>[]; error: string | nu
|
|||
|
||||
const combined = [...builtInModels, ...customModels];
|
||||
|
||||
// Update github-copilot base URL based on OAuth token or enterprise domain
|
||||
const copilotCreds = loadOAuthCredentials("github-copilot");
|
||||
if (copilotCreds?.enterpriseUrl) {
|
||||
const domain = normalizeDomain(copilotCreds.enterpriseUrl);
|
||||
if (domain) {
|
||||
const baseUrl = getGitHubCopilotBaseUrl(domain);
|
||||
return {
|
||||
models: combined.map((m) =>
|
||||
m.provider === "github-copilot" && m.baseUrl === "https://api.githubcopilot.com" ? { ...m, baseUrl } : m,
|
||||
),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
if (copilotCreds) {
|
||||
const domain = copilotCreds.enterpriseUrl ? normalizeDomain(copilotCreds.enterpriseUrl) : undefined;
|
||||
const baseUrl = getGitHubCopilotBaseUrl(copilotCreds.access, domain ?? undefined);
|
||||
return {
|
||||
models: combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m)),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return { models: combined, error: null };
|
||||
|
|
@ -288,23 +285,31 @@ export async function getApiKeyForModel(model: Model<Api>): Promise<string | und
|
|||
}
|
||||
|
||||
if (model.provider === "github-copilot") {
|
||||
// 1. Check OAuth storage (from device flow login)
|
||||
const oauthToken = await getOAuthToken("github-copilot");
|
||||
if (oauthToken) {
|
||||
return oauthToken;
|
||||
}
|
||||
|
||||
// 2. Use GitHub token directly (works with copilot scope on github.com)
|
||||
const githubToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
||||
if (!githubToken) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 3. For enterprise, exchange token for short-lived Copilot token
|
||||
const enterpriseDomain = process.env.COPILOT_ENTERPRISE_URL
|
||||
? normalizeDomain(process.env.COPILOT_ENTERPRISE_URL)
|
||||
: undefined;
|
||||
|
||||
const creds = await refreshGitHubCopilotToken(githubToken, enterpriseDomain || undefined);
|
||||
saveOAuthCredentials("github-copilot", creds);
|
||||
return creds.access;
|
||||
if (enterpriseDomain) {
|
||||
const creds = await refreshGitHubCopilotToken(githubToken, enterpriseDomain);
|
||||
saveOAuthCredentials("github-copilot", creds);
|
||||
return creds.access;
|
||||
}
|
||||
|
||||
// 4. For github.com, use token directly
|
||||
return githubToken;
|
||||
}
|
||||
|
||||
// For built-in providers, use getApiKey from @mariozechner/pi-ai
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|||
|
||||
const COPILOT_HEADERS = {
|
||||
"User-Agent": "GitHubCopilotChat/0.35.0",
|
||||
"Editor-Version": "vscode/1.105.1",
|
||||
"Editor-Version": "vscode/1.107.0",
|
||||
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
||||
"Copilot-Integration-Id": "copilot-developer-cli",
|
||||
"Openai-Intent": "conversation-edits",
|
||||
"X-Initiator": "agent",
|
||||
"Copilot-Integration-Id": "vscode-chat",
|
||||
} as const;
|
||||
|
||||
type DeviceCodeResponse = {
|
||||
|
|
@ -54,9 +52,29 @@ function getUrls(domain: string): {
|
|||
};
|
||||
}
|
||||
|
||||
export function getGitHubCopilotBaseUrl(enterpriseDomain?: string): string {
|
||||
if (!enterpriseDomain) return "https://api.githubcopilot.com";
|
||||
return `https://copilot-api.${enterpriseDomain}`;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function getBaseUrlFromToken(token: string): string | null {
|
||||
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}`;
|
||||
}
|
||||
|
||||
export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
|
||||
// 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";
|
||||
}
|
||||
|
||||
async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue