11import type { AppConfig } from "./config.js" ;
2+ import type { ClientOptions } from "openai" ;
3+ import type * as Core from "openai/core" ;
24
35import {
46 getBaseUrl ,
@@ -9,6 +11,7 @@ import {
911 OPENAI_PROJECT ,
1012} from "./config.js" ;
1113import OpenAI , { AzureOpenAI } from "openai" ;
14+ import * as Errors from "openai/error" ;
1215
1316type OpenAIClientConfig = {
1417 provider : string ;
@@ -42,10 +45,166 @@ export function createOpenAIClient(
4245 } ) ;
4346 }
4447
48+ if ( config . provider ?. toLowerCase ( ) === "githubcopilot" ) {
49+ return new GithubCopilotClient ( {
50+ apiKey : getApiKey ( config . provider ) ,
51+ baseURL : getBaseUrl ( config . provider ) ,
52+ timeout : OPENAI_TIMEOUT_MS ,
53+ defaultHeaders : headers ,
54+ } ) ;
55+ }
56+
4557 return new OpenAI ( {
4658 apiKey : getApiKey ( config . provider ) ,
4759 baseURL : getBaseUrl ( config . provider ) ,
4860 timeout : OPENAI_TIMEOUT_MS ,
4961 defaultHeaders : headers ,
5062 } ) ;
5163}
64+
65+ export class GithubCopilotClient extends OpenAI {
66+ private copilotToken : string | null = null ;
67+ private copilotTokenExpiration = new Date ( ) ;
68+ private githubAPIKey : string ;
69+
70+ constructor ( opts : ClientOptions = { } ) {
71+ super ( opts ) ;
72+ if ( ! opts . apiKey ) {
73+ throw new Errors . OpenAIError ( "missing github copilot token" ) ;
74+ }
75+ this . githubAPIKey = opts . apiKey ;
76+ }
77+
78+ private async _getGithubCopilotToken ( ) : Promise < string | undefined > {
79+ if (
80+ this . copilotToken &&
81+ this . copilotTokenExpiration . getTime ( ) > Date . now ( )
82+ ) {
83+ return this . copilotToken ;
84+ }
85+ const resp = await fetch (
86+ "https://api.github.com/copilot_internal/v2/token" ,
87+ {
88+ method : "GET" ,
89+ headers : GithubCopilotClient . _mergeGithubHeaders ( {
90+ "Authorization" : `bearer ${ this . githubAPIKey } ` ,
91+ "Accept" : "application/json" ,
92+ "Content-Type" : "application/json" ,
93+ } ) ,
94+ } ,
95+ ) ;
96+ if ( ! resp . ok ) {
97+ const text = await resp . text ( ) ;
98+ throw new Error ( "unable to get github copilot auth token: " + text ) ;
99+ }
100+ const text = await resp . text ( ) ;
101+ const { token, refresh_in } = JSON . parse ( text ) ;
102+ if ( typeof token !== "string" || typeof refresh_in !== "number" ) {
103+ throw new Errors . OpenAIError (
104+ `unexpected response from copilot auth: ${ text } ` ,
105+ ) ;
106+ }
107+ this . copilotToken = token ;
108+ this . copilotTokenExpiration = new Date ( Date . now ( ) + refresh_in * 1000 ) ;
109+ return token ;
110+ }
111+
112+ protected override authHeaders (
113+ _opts : Core . FinalRequestOptions ,
114+ ) : Core . Headers {
115+ return { } ;
116+ }
117+
118+ protected override async prepareOptions (
119+ opts : Core . FinalRequestOptions < unknown > ,
120+ ) : Promise < void > {
121+ const token = await this . _getGithubCopilotToken ( ) ;
122+ opts . headers ??= { } ;
123+ if ( token ) {
124+ opts . headers [ "Authorization" ] = `Bearer ${ token } ` ;
125+ opts . headers = GithubCopilotClient . _mergeGithubHeaders ( opts . headers ) ;
126+ } else {
127+ throw new Errors . OpenAIError ( "Unable to handle auth" ) ;
128+ }
129+ return super . prepareOptions ( opts ) ;
130+ }
131+
132+ static async getLoginURL ( ) : Promise < {
133+ device_code : string ;
134+ user_code : string ;
135+ verification_uri : string ;
136+ } > {
137+ const resp = await fetch ( "https://github.com/login/device/code" , {
138+ method : "POST" ,
139+ headers : this . _mergeGithubHeaders ( {
140+ "Content-Type" : "application/json" ,
141+ "accept" : "application/json" ,
142+ } ) ,
143+ body : JSON . stringify ( {
144+ client_id : "Iv1.b507a08c87ecfe98" ,
145+ scope : "read:user" ,
146+ } ) ,
147+ } ) ;
148+ if ( ! resp . ok ) {
149+ const text = await resp . text ( ) ;
150+ throw new Errors . OpenAIError ( "Unable to get login device code: " + text ) ;
151+ }
152+ return resp . json ( ) ;
153+ }
154+
155+ static async pollForAccessToken ( deviceCode : string ) : Promise < string > {
156+ /*eslint no-await-in-loop: "off"*/
157+ const MAX_ATTEMPTS = 36 ;
158+ let lastErr : unknown = null ;
159+ for ( let i = 0 ; i < MAX_ATTEMPTS ; ++ i ) {
160+ try {
161+ const resp = await fetch (
162+ "https://github.com/login/oauth/access_token" ,
163+ {
164+ method : "POST" ,
165+ headers : this . _mergeGithubHeaders ( {
166+ "Content-Type" : "application/json" ,
167+ "accept" : "application/json" ,
168+ } ) ,
169+ body : JSON . stringify ( {
170+ client_id : "Iv1.b507a08c87ecfe98" ,
171+ device_code : deviceCode ,
172+ grant_type : "urn:ietf:params:oauth:grant-type:device_code" ,
173+ } ) ,
174+ } ,
175+ ) ;
176+ if ( ! resp . ok ) {
177+ continue ;
178+ }
179+ const info = await resp . json ( ) ;
180+ if ( info . access_token ) {
181+ return info . access_token as string ;
182+ } else if ( info . error === "authorization_pending" ) {
183+ lastErr = null ;
184+ } else {
185+ throw new Errors . OpenAIError (
186+ "unexpected response when polling for access token: " +
187+ JSON . stringify ( info ) ,
188+ ) ;
189+ }
190+ } catch ( err ) {
191+ lastErr = err ;
192+ }
193+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5_000 ) ) ;
194+ }
195+ throw new Errors . OpenAIError (
196+ "timed out waiting for access token" ,
197+ lastErr != null ? { cause : lastErr } : { } ,
198+ ) ;
199+ }
200+
201+ private static _mergeGithubHeaders <
202+ T extends Core . Headers | Record < string , string > ,
203+ > ( headers : T ) : T {
204+ const copy = { ...headers } as Record < string , string > & T ;
205+ copy [ "User-Agent" ] = "GithubCopilot/1.155.0" ;
206+ copy [ "editor-version" ] = "vscode/1.85.1" ;
207+ copy [ "editor-plugin-version" ] = "copilot/1.155.0" ;
208+ return copy as T ;
209+ }
210+ }
0 commit comments