diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index fee36663..d8b5bb02 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -29,7 +29,7 @@ import { isSignedIn } from '@x/core/dist/account/account.js'; import { listGatewayModels } from '@x/core/dist/models/gateway.js'; import type { IModelConfigRepo } from '@x/core/dist/models/repo.js'; import type { IOAuthRepo } from '@x/core/dist/auth/repo.js'; -import { startGitHubCopilotAuthentication, getGitHubCopilotAccessToken, isGitHubCopilotAuthenticated, disconnectGitHubCopilot } from '@x/core/dist/auth/github-copilot-auth.js'; +import { startGitHubCopilotAuthentication, isGitHubCopilotAuthenticated, disconnectGitHubCopilot } from '@x/core/dist/auth/github-copilot-auth.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js'; import { ISlackConfigRepo } from '@x/core/dist/slack/repo.js'; diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index 647656e4..4af34966 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -214,6 +214,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { const [configLoading, setConfigLoading] = useState(true) const [showMoreProviders, setShowMoreProviders] = useState(false) const [githubCopilotCode, setGithubCopilotCode] = useState<{ userCode: string; verificationUri: string } | null>(null) + const [githubCopilotAuthenticated, setGithubCopilotAuthenticated] = useState(false) const activeConfig = providerConfigs[provider] const showApiKey = provider === "openai" || provider === "anthropic" || provider === "google" || provider === "openrouter" || provider === "aigateway" || provider === "openai-compatible" @@ -223,7 +224,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { const isLocalProvider = provider === "ollama" || provider === "openai-compatible" const isGitHubCopilot = provider === "github-copilot" const modelsForProvider = modelsCatalog[provider] || [] - const showModelInput = isLocalProvider || modelsForProvider.length === 0 || isGitHubCopilot + const showModelInput = isLocalProvider || modelsForProvider.length === 0 const isMoreProvider = moreProviders.some(p => p.id === provider) const primaryModel = activeConfig.models[0] || "" @@ -337,6 +338,23 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { loadCurrentConfig() }, [dialogOpen]) + // Check GitHub Copilot auth status + useEffect(() => { + if (!dialogOpen || provider !== "github-copilot") return; + + async function checkCopilotAuth() { + try { + const result = await window.ipc.invoke("github-copilot:isAuthenticated", null); + setGithubCopilotAuthenticated(!!result.authenticated); + } catch (error) { + console.error("Error checking Copilot auth:", error); + setGithubCopilotAuthenticated(false); + } + } + + checkCopilotAuth(); + }, [dialogOpen, provider]); + // Load models catalog useEffect(() => { if (!dialogOpen) return @@ -693,7 +711,44 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
Authentication - {!githubCopilotCode ? ( + {githubCopilotAuthenticated ? ( +
+
+
+ + Connected to GitHub +
+ +
+

+ Your device is authenticated. You can select any model from the dropdown above to start using GitHub Copilot. +

+
+ ) : !githubCopilotCode ? ( <>

GitHub Copilot uses Device Flow OAuth. Click below to get a device code. @@ -786,6 +841,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { if (authCheck.authenticated) { setTestState({ status: "success" }); setGithubCopilotCode(null); + setGithubCopilotAuthenticated(true); toast.success("GitHub Copilot authenticated!"); // Reload models diff --git a/apps/x/packages/core/src/auth/github-copilot-auth.ts b/apps/x/packages/core/src/auth/github-copilot-auth.ts index 03d5490f..f62df9ea 100644 --- a/apps/x/packages/core/src/auth/github-copilot-auth.ts +++ b/apps/x/packages/core/src/auth/github-copilot-auth.ts @@ -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 { + // 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 { console.log('[GitHub Copilot] Disconnecting...'); const oauthRepo = container.resolve('oauthRepo'); await oauthRepo.delete(PROVIDER_NAME); + cachedCopilotToken = null; console.log('[GitHub Copilot] Disconnected successfully'); } diff --git a/apps/x/packages/core/src/auth/github-copilot-models.ts b/apps/x/packages/core/src/auth/github-copilot-models.ts index 756584bb..3851f5ee 100644 --- a/apps/x/packages/core/src/auth/github-copilot-models.ts +++ b/apps/x/packages/core/src/auth/github-copilot-models.ts @@ -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) { diff --git a/apps/x/packages/core/src/models/gateway.ts b/apps/x/packages/core/src/models/gateway.ts index a18a37f5..8932f9fc 100644 --- a/apps/x/packages/core/src/models/gateway.ts +++ b/apps/x/packages/core/src/models/gateway.ts @@ -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 { 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 }; } diff --git a/apps/x/packages/core/src/models/models-dev.ts b/apps/x/packages/core/src/models/models-dev.ts index 5036aeb0..b29bda63 100644 --- a/apps/x/packages/core/src/models/models-dev.ts +++ b/apps/x/packages/core/src/models/models-dev.ts @@ -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)" } ], });