feat: add GitHub Copilot model provider with OAuth device flow

Add a new 'github' model provider backed by the Copilot API
(api.githubcopilot.com), with OAuth device flow for authentication.

- New src/auth/github.ts: device flow login, token storage at
  ~/.config/flynn/auth.json with 0600 permissions
- New src/models/github.ts: OpenAI-compatible client with streaming,
  tool calling, and Copilot-specific headers
- Add 'github' to provider enum in config schema
- Register provider in daemon factory and TUI client factory
- Refactor TUI to use provider-agnostic client factory (was hardcoded
  to AnthropicClient for all tiers)
- Add /login command to TUI for interactive OAuth authorization
- Add Copilot model cost tracking entries
This commit is contained in:
William Valentin
2026-02-06 22:26:52 -08:00
parent a515912537
commit f363717f5f
10 changed files with 493 additions and 43 deletions
+30
View File
@@ -7,6 +7,7 @@ import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, ge
import { renderMarkdown } from './markdown.js';
import type { ModelConfig } from '../../config/schema.js';
import { OllamaClient, LlamaCppClient } from '../../models/index.js';
import { loginGitHub } from '../../auth/index.js';
export { parseCommand, type Command };
@@ -186,6 +187,10 @@ export class MinimalTui {
this.handleBackendCommand(command.provider);
break;
case 'login':
await this.handleLoginCommand(command.provider);
break;
case 'transfer':
this.config.onTransfer?.(command.target);
break;
@@ -256,6 +261,31 @@ export class MinimalTui {
console.log(`Switched to backend: ${provider}\n`);
}
private async handleLoginCommand(provider?: string): Promise<void> {
const target = provider ?? 'github';
if (target !== 'github') {
console.log(`${colors.gray}Unknown login provider:${colors.reset} ${target}. Only 'github' is supported.\n`);
return;
}
console.log(`${colors.gray}Starting GitHub OAuth device flow...${colors.reset}`);
try {
await loginGitHub((userCode, verificationUri) => {
console.log('');
console.log(`${colors.gray}Please visit:${colors.reset} ${verificationUri}`);
console.log(`${colors.gray}and enter code:${colors.reset} ${userCode}`);
console.log('');
console.log(`${colors.gray}Waiting for authorization...${colors.reset}`);
});
console.log(`${colors.gray}GitHub authentication successful! Token stored.${colors.reset}\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`${colors.gray}GitHub login failed:${colors.reset} ${message}\n`);
}
}
private getAvailableBackends(): string[] {
const backends: string[] = [];
if (this.config.currentLocalProvider) {