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:
@@ -8,6 +8,7 @@ export type Command =
|
||||
| { type: 'usage' }
|
||||
| { type: 'model'; name?: string }
|
||||
| { type: 'backend'; provider?: string }
|
||||
| { type: 'login'; provider?: string }
|
||||
| { type: 'transfer'; target: string }
|
||||
| { type: 'message'; content: string };
|
||||
|
||||
@@ -74,6 +75,15 @@ export function parseCommand(input: string): Command | null {
|
||||
return { type: 'transfer', target };
|
||||
}
|
||||
|
||||
// Login
|
||||
if (trimmed === '/login') {
|
||||
return { type: 'login' };
|
||||
}
|
||||
if (trimmed.startsWith('/login ')) {
|
||||
const provider = trimmed.slice('/login '.length).trim();
|
||||
return { type: 'login', provider: provider || undefined };
|
||||
}
|
||||
|
||||
// Regular message
|
||||
return { type: 'message', content: trimmed };
|
||||
}
|
||||
@@ -84,6 +94,7 @@ Commands:
|
||||
/help, /? Show this help
|
||||
/model [name] Show or switch model (local, default, fast, complex)
|
||||
/backend [provider] Show or switch local backend (ollama, llamacpp)
|
||||
/login [provider] Authenticate with GitHub
|
||||
/reset, /clear, /new Clear conversation history
|
||||
/compact Compact conversation history
|
||||
/usage Show token usage and estimated cost
|
||||
@@ -109,6 +120,7 @@ export const SLASH_COMMANDS = [
|
||||
'/status',
|
||||
'/fullscreen',
|
||||
'/fs',
|
||||
'/login',
|
||||
'/transfer',
|
||||
'/quit',
|
||||
'/exit',
|
||||
@@ -127,6 +139,7 @@ export const COMMAND_TOOLTIPS: Record<string, string> = {
|
||||
'/status': 'Show session info and token usage',
|
||||
'/fullscreen': 'Switch to fullscreen mode',
|
||||
'/fs': 'Switch to fullscreen mode',
|
||||
'/login': 'Authenticate with GitHub (OAuth device flow)',
|
||||
'/transfer': 'Transfer session to another frontend',
|
||||
'/quit': 'Exit TUI',
|
||||
'/exit': 'Exit TUI',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user