feat: add auto-login for GitHub Copilot when no token is available
GitHubModelsClient now lazily resolves tokens at first API call. If no token exists (env var, stored OAuth, or config), it triggers the OAuth device flow automatically via an onLoginRequired callback wired in both the TUI and daemon entry points.
This commit is contained in:
+14
-1
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>).provider}`);
|
||||
|
||||
+55
-4
@@ -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<string>;
|
||||
}
|
||||
|
||||
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<string>;
|
||||
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<void> {
|
||||
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<ChatResponse> {
|
||||
await this.ensureToken();
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [];
|
||||
|
||||
if (request.system) {
|
||||
@@ -125,6 +175,7 @@ export class GitHubModelsClient implements ModelClient {
|
||||
}
|
||||
|
||||
async *chatStream(request: ChatRequest): AsyncIterable<ChatStreamEvent> {
|
||||
await this.ensureToken();
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [];
|
||||
|
||||
if (request.system) {
|
||||
|
||||
Reference in New Issue
Block a user