diff --git a/src/cli/tui.ts b/src/cli/tui.ts index b630423..8141bac 100644 --- a/src/cli/tui.ts +++ b/src/cli/tui.ts @@ -74,7 +74,20 @@ export function registerTuiCommand(program: Command): void { case 'bedrock': return new BedrockClient({ model: cfg.model, region: cfg.endpoint, accessKeyId: cfg.api_key, secretAccessKey: cfg.auth_token }); case 'github': - return new GitHubModelsClient({ model: cfg.model, apiKey: cfg.api_key, endpoint: cfg.endpoint }); + return new GitHubModelsClient({ + model: cfg.model, + apiKey: cfg.api_key, + endpoint: cfg.endpoint, + onLoginRequired: async () => { + const { loginGitHub } = await import('../auth/index.js'); + console.log('\nGitHub authentication required. Starting login flow...'); + return loginGitHub((userCode, verificationUri) => { + console.log(`\nVisit: ${verificationUri}`); + console.log(`Enter code: ${userCode}\n`); + console.log('Waiting for authorization...'); + }); + }, + }); default: throw new Error(`Unknown provider: ${cfg.provider}`); } diff --git a/src/daemon/index.ts b/src/daemon/index.ts index b1a3b53..b594100 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -114,6 +114,13 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient { model: cfg.model, apiKey: cfg.api_key, endpoint: cfg.endpoint, + onLoginRequired: async () => { + const { loginGitHub } = await import('../auth/index.js'); + return loginGitHub((userCode, verificationUri) => { + console.log(`GitHub login required. Visit: ${verificationUri}`); + console.log(`Enter code: ${userCode}`); + }); + }, }); default: throw new Error(`Unknown model provider: ${(cfg as Record).provider}`); diff --git a/src/models/github.ts b/src/models/github.ts index 1570704..1c50fac 100644 --- a/src/models/github.ts +++ b/src/models/github.ts @@ -6,7 +6,13 @@ export interface GitHubModelsClientConfig { apiKey?: string; // GitHub PAT or gh auth token. Falls back to GITHUB_TOKEN env var model: string; // e.g., 'gpt-4o' or 'claude-sonnet-4' maxTokens?: number; - endpoint?: string; // Override base URL (default: https://models.github.ai/inference) + endpoint?: string; // Override base URL (default: https://api.githubcopilot.com) + /** + * Optional callback invoked when no token is available at API call time. + * Should return a valid token (e.g. by running the OAuth device flow). + * If not provided and no token is available, API calls will fail with auth errors. + */ + onLoginRequired?: () => Promise; } const DEFAULT_ENDPOINT = 'https://api.githubcopilot.com'; @@ -39,14 +45,19 @@ export class GitHubModelsClient implements ModelClient { private client: OpenAI; private model: string; private defaultMaxTokens: number; + private baseURL: string; + private onLoginRequired?: () => Promise; + private tokenResolved = false; constructor(config: GitHubModelsClientConfig) { const apiKey = config.apiKey ?? getGitHubToken() ?? ''; - const baseURL = config.endpoint ?? DEFAULT_ENDPOINT; + this.baseURL = config.endpoint ?? DEFAULT_ENDPOINT; + this.onLoginRequired = config.onLoginRequired; + this.tokenResolved = !!apiKey; this.client = new OpenAI({ - apiKey, - baseURL, + apiKey: apiKey || 'placeholder', + baseURL: this.baseURL, defaultHeaders: { 'X-GitHub-Api-Version': '2022-11-28', 'Openai-Intent': 'conversation-edits', @@ -56,7 +67,46 @@ export class GitHubModelsClient implements ModelClient { this.defaultMaxTokens = config.maxTokens ?? 4096; } + /** + * Ensure we have a valid token before making an API call. + * If no token was resolved at construction time and an onLoginRequired + * callback is provided, invoke it to obtain a token (e.g. via OAuth device flow). + */ + private async ensureToken(): Promise { + if (this.tokenResolved) return; + + // Try resolving again (user might have logged in via /login since construction) + const token = getGitHubToken(); + if (token) { + this.rebuildClient(token); + return; + } + + // Trigger auto-login if callback provided + if (this.onLoginRequired) { + const newToken = await this.onLoginRequired(); + this.rebuildClient(newToken); + return; + } + + // No token and no callback — the API call will fail with an auth error + } + + /** Rebuild the OpenAI client with a new API key. */ + private rebuildClient(apiKey: string): void { + this.client = new OpenAI({ + apiKey, + baseURL: this.baseURL, + defaultHeaders: { + 'X-GitHub-Api-Version': '2022-11-28', + 'Openai-Intent': 'conversation-edits', + }, + }); + this.tokenResolved = true; + } + async chat(request: ChatRequest): Promise { + await this.ensureToken(); const messages: OpenAI.ChatCompletionMessageParam[] = []; if (request.system) { @@ -125,6 +175,7 @@ export class GitHubModelsClient implements ModelClient { } async *chatStream(request: ChatRequest): AsyncIterable { + await this.ensureToken(); const messages: OpenAI.ChatCompletionMessageParam[] = []; if (request.system) {