Feat: Complete GitHub Copilot Student Integration via Device Flow and internal API exchange

This commit is contained in:
Rowboat Developer 2026-04-17 11:51:23 -05:00
parent 97853c4b4c
commit 61af7f3c58
6 changed files with 160 additions and 26 deletions

View file

@ -180,9 +180,45 @@ export async function getGitHubCopilotAuthStatus(): Promise<{
/**
* Disconnect GitHub Copilot (remove stored tokens)
*/
/**
* Get GitHub Copilot API Token (exchange OAuth token for Copilot JWT)
*/
let cachedCopilotToken: { token: string; expiresAt: number } | null = null;
export async function getGitHubCopilotApiToken(): Promise<string> {
// Return cached token if valid (with 5 min buffer)
if (cachedCopilotToken && cachedCopilotToken.expiresAt > Date.now() / 1000 + 300) {
return cachedCopilotToken.token;
}
const oauthToken = await getGitHubCopilotAccessToken();
const response = await fetch('https://api.github.com/copilot_internal/v2/token', {
headers: {
'Authorization': `Bearer ${oauthToken}`,
'Accept': 'application/json',
'User-Agent': 'GithubCopilot/1.155.0'
}
});
if (!response.ok) {
const err = await response.text();
throw new Error(`Failed to get Copilot token: ${response.status} ${err}`);
}
const data = await response.json() as { token: string; expires_at: number };
cachedCopilotToken = {
token: data.token,
expiresAt: data.expires_at
};
return data.token;
}
export async function disconnectGitHubCopilot(): Promise<void> {
console.log('[GitHub Copilot] Disconnecting...');
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
await oauthRepo.delete(PROVIDER_NAME);
cachedCopilotToken = null;
console.log('[GitHub Copilot] Disconnected successfully');
}

View file

@ -4,23 +4,31 @@
* Handles GitHub Copilot model discovery and LLM provider initialization
*/
import { getGitHubCopilotAccessToken, isGitHubCopilotAuthenticated } from './github-copilot-auth.js';
import { getGitHubCopilotApiToken, isGitHubCopilotAuthenticated } from './github-copilot-auth.js';
import { ProviderV2 } from '@ai-sdk/provider';
import { createOpenAI } from '@ai-sdk/openai';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import z from 'zod';
import { LlmProvider } from '@x/shared/dist/models.js';
// GitHub Copilot API endpoint
const GITHUB_COPILOT_API_BASE = 'https://models.github.com/api/openai/';
const GITHUB_COPILOT_API_BASE = 'https://api.githubcopilot.com/';
// List of models available through GitHub Copilot
// Based on GitHub Copilot API documentation
// https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions
export const GITHUB_COPILOT_MODELS = [
'gpt-4o', // GPT-4 Optimized (recommended)
'gpt-4-turbo', // GPT-4 Turbo
'gpt-3.5-turbo', // GPT-3.5 Turbo (fastest)
'claude-3.5-sonnet', // Claude 3.5 Sonnet (if available in plan)
'gpt-5.4-mini',
'gpt-5-mini',
'grok-code-fast-1',
'claude-haiku-4.5',
'gemini-3-flash-preview',
'gpt-5.2',
'gpt-4.1',
'gpt-4o',
'gemini-3.1-pro-preview',
'gpt-5.2-codex',
'gpt-5.3-codex',
'gemini-2.5-pro'
] as const;
export type GitHubCopilotModel = typeof GITHUB_COPILOT_MODELS[number];
@ -57,16 +65,20 @@ export async function createGitHubCopilotProvider(
);
}
// Get access token (will handle refresh if needed)
const accessToken = await getGitHubCopilotAccessToken();
// Get Copilot API token (handles refresh if needed)
const accessToken = await getGitHubCopilotApiToken();
// Create OpenAI-compatible provider with GitHub Copilot endpoint
return createOpenAI({
return createOpenAICompatible({
name: "github-copilot",
apiKey: accessToken,
baseURL: config.baseURL || GITHUB_COPILOT_API_BASE,
headers: {
...config.headers,
'user-agent': 'Rowboat/1.0',
'Editor-Version': 'vscode/1.88.0',
'Editor-Plugin-Version': 'copilot-chat/0.14.0',
'User-Agent': 'GitHubCopilotChat/0.14.0',
'Accept': '*/*',
},
});
}
@ -88,7 +100,7 @@ export async function testGitHubCopilotConnection(): Promise<{ success: boolean;
}
// Try to get access token
await getGitHubCopilotAccessToken();
await getGitHubCopilotApiToken();
return { success: true };
} catch (error) {

View file

@ -2,6 +2,7 @@ import { ProviderV2 } from '@ai-sdk/provider';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { getAccessToken } from '../auth/tokens.js';
import { API_URL } from '../config/env.js';
import { isGitHubCopilotAuthenticated } from '../auth/github-copilot-auth.js';
export async function getGatewayProvider(): Promise<ProviderV2> {
const accessToken = await getAccessToken();
@ -31,11 +32,32 @@ export async function listGatewayModels(): Promise<{ providers: ProviderSummary[
}
const body = await response.json() as { data: Array<{ id: string }> };
const models = body.data.map((m) => ({ id: m.id }));
return {
providers: [{
id: 'rowboat',
name: 'Rowboat',
models,
}],
};
const providers: ProviderSummary[] = [{
id: 'rowboat',
name: 'Rowboat',
models,
}];
// Add GitHub Copilot models always so they appear in UI
providers.push({
id: "github-copilot",
name: "GitHub Copilot Student",
models: [
{ id: "gpt-5.4-mini", name: "GPT-5.4 mini" },
{ id: "gpt-5-mini", name: "GPT-5 mini" },
{ id: "grok-code-fast-1", name: "Grok Code Fast 1" },
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash (Preview)" },
{ id: "gpt-5.2", name: "GPT-5.2" },
{ id: "gpt-4.1", name: "GPT-4.1" },
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro (Preview)" },
{ id: "gpt-5.2-codex", name: "GPT-5.2-Codex" },
{ id: "gpt-5.3-codex", name: "GPT-5.3-Codex" },
{ id: "gemini-2.5-pro-preview", name: "Gemini 2.5 Pro (Preview)" }
],
});
return { providers };
}

View file

@ -227,10 +227,18 @@ export async function listOnboardingModels(): Promise<{ providers: ProviderSumma
id: "github-copilot",
name: "GitHub Copilot Student",
models: [
{ id: "gpt-4o", name: "GPT-4o (Recommended)" },
{ id: "gpt-4-turbo", name: "GPT-4 Turbo" },
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo (Fastest)" },
{ id: "claude-3.5-sonnet", name: "Claude 3.5 Sonnet" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 mini" },
{ id: "gpt-5-mini", name: "GPT-5 mini" },
{ id: "grok-code-fast-1", name: "Grok Code Fast 1" },
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash (Preview)" },
{ id: "gpt-5.2", name: "GPT-5.2" },
{ id: "gpt-4.1", name: "GPT-4.1" },
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro (Preview)" },
{ id: "gpt-5.2-codex", name: "GPT-5.2-Codex" },
{ id: "gpt-5.3-codex", name: "GPT-5.3-Codex" },
{ id: "gemini-2.5-pro-preview", name: "Gemini 2.5 Pro (Preview)" }
],
});