1+ local notify = require (' CopilotChat.notify' )
12local utils = require (' CopilotChat.utils' )
3+ local plenary_utils = require (' plenary.async.util' )
24
35local EDITOR_VERSION = ' Neovim/' .. vim .version ().major .. ' .' .. vim .version ().minor .. ' .' .. vim .version ().patch
46
5- local cached_github_token = nil
7+ local token_cache = nil
8+ local unsaved_token_cache = {}
9+ local function load_tokens ()
10+ if token_cache then
11+ return token_cache
12+ end
13+
14+ local config_path = vim .fs .normalize (vim .fn .stdpath (' data' ) .. ' /copilot_chat' )
15+ local cache_file = config_path .. ' /tokens.json'
16+ local file = utils .read_file (cache_file )
17+ if file then
18+ token_cache = vim .json .decode (file )
19+ else
20+ token_cache = {}
21+ end
22+
23+ return token_cache
24+ end
25+
26+ local function get_token (tag )
27+ if unsaved_token_cache [tag ] then
28+ return unsaved_token_cache [tag ]
29+ end
30+
31+ local tokens = load_tokens ()
32+ return tokens [tag ]
33+ end
634
7- local function config_path ( )
8- local config = vim . fs . normalize ( ' $XDG_CONFIG_HOME ' )
9- if config and vim . uv . fs_stat ( config ) then
10- return config
35+ local function set_token ( tag , token , save )
36+ if not save then
37+ unsaved_token_cache [ tag ] = token
38+ return token
1139 end
12- if vim .fn .has (' win32' ) > 0 then
13- config = vim .fs .normalize (' $LOCALAPPDATA' )
14- if not config or not vim .uv .fs_stat (config ) then
15- config = vim .fs .normalize (' $HOME/AppData/Local' )
40+
41+ local tokens = load_tokens ()
42+ tokens [tag ] = token
43+ local config_path = vim .fs .normalize (vim .fn .stdpath (' data' ) .. ' /copilot_chat' )
44+ utils .write_file (config_path .. ' /tokens.json' , vim .json .encode (tokens ))
45+ return token
46+ end
47+
48+ --- Get the github token using device flow
49+ --- @return string
50+ local function github_device_flow (tag , client_id , scope )
51+ local function request_device_code ()
52+ local res = utils .curl_post (' https://github.com/login/device/code' , {
53+ body = {
54+ client_id = client_id ,
55+ scope = scope ,
56+ },
57+ headers = { [' Accept' ] = ' application/json' },
58+ })
59+
60+ local data = vim .json .decode (res .body )
61+ return data
62+ end
63+
64+ local function poll_for_token (device_code , interval )
65+ while true do
66+ plenary_utils .sleep (interval * 1000 )
67+
68+ local res = utils .curl_post (' https://github.com/login/oauth/access_token' , {
69+ body = {
70+ client_id = client_id ,
71+ device_code = device_code ,
72+ grant_type = ' urn:ietf:params:oauth:grant-type:device_code' ,
73+ },
74+ headers = { [' Accept' ] = ' application/json' },
75+ })
76+ local data = vim .json .decode (res .body )
77+ if data .access_token then
78+ return data .access_token
79+ elseif data .error ~= ' authorization_pending' then
80+ error (' Auth error: ' .. (data .error or ' unknown' ))
81+ end
1682 end
17- else
18- config = vim .fs .normalize (' $HOME/.config' )
1983 end
20- if config and vim .uv .fs_stat (config ) then
21- return config
84+
85+ local token = get_token (tag )
86+ if token then
87+ return token
2288 end
89+
90+ local code_data = request_device_code ()
91+ notify .publish (
92+ notify .MESSAGE ,
93+ ' [' .. tag .. ' ] Visit ' .. code_data .verification_uri .. ' and enter code: ' .. code_data .user_code
94+ )
95+ notify .publish (notify .STATUS , ' [' .. tag .. ' ] Waiting for GitHub models authorization...' )
96+ token = poll_for_token (code_data .device_code , code_data .interval )
97+ return set_token (tag , token , true )
2398end
2499
25100--- Get the github copilot oauth cached token (gu_ token)
26101--- @return string
27- local function get_github_token ()
28- if cached_github_token then
29- return cached_github_token
102+ local function get_github_token (tag )
103+ local function config_path ()
104+ local config = vim .fs .normalize (' $XDG_CONFIG_HOME' )
105+ if config and vim .uv .fs_stat (config ) then
106+ return config
107+ end
108+ if vim .fn .has (' win32' ) > 0 then
109+ config = vim .fs .normalize (' $LOCALAPPDATA' )
110+ if not config or not vim .uv .fs_stat (config ) then
111+ config = vim .fs .normalize (' $HOME/AppData/Local' )
112+ end
113+ else
114+ config = vim .fs .normalize (' $HOME/.config' )
115+ end
116+ if config and vim .uv .fs_stat (config ) then
117+ return config
118+ end
119+ end
120+
121+ local token = get_token (tag )
122+ if token then
123+ return token
30124 end
31125
32126 -- loading token from the environment only in GitHub Codespaces
33- local token = os.getenv (' GITHUB_TOKEN' )
34127 local codespaces = os.getenv (' CODESPACES' )
128+ token = os.getenv (' GITHUB_TOKEN' )
35129 if token and codespaces then
36- cached_github_token = token
37- return token
130+ return set_token (tag , token , false )
38131 end
39132
40133 -- loading token from the file
41134 local config_path = config_path ()
42- if not config_path then
43- error (' Failed to find config path for GitHub token' )
44- end
135+ if config_path then
136+ -- token can be sometimes in apps.json sometimes in hosts.json
137+ local file_paths = {
138+ config_path .. ' /github-copilot/hosts.json' ,
139+ config_path .. ' /github-copilot/apps.json' ,
140+ }
45141
46- -- token can be sometimes in apps.json sometimes in hosts.json
47- local file_paths = {
48- config_path .. ' /github-copilot/hosts.json' ,
49- config_path .. ' /github-copilot/apps.json' ,
50- }
51-
52- for _ , file_path in ipairs (file_paths ) do
53- local file_data = utils .read_file (file_path )
54- if file_data then
55- local parsed_data = utils .json_decode (file_data )
56- if parsed_data then
57- for key , value in pairs (parsed_data ) do
58- if string.find (key , ' github.com' ) then
59- cached_github_token = value .oauth_token
60- return value .oauth_token
142+ for _ , file_path in ipairs (file_paths ) do
143+ local file_data = utils .read_file (file_path )
144+ if file_data then
145+ local parsed_data = utils .json_decode (file_data )
146+ if parsed_data then
147+ for key , value in pairs (parsed_data ) do
148+ if string.find (key , ' github.com' ) and value and value .oauth_token then
149+ return set_token (tag , value .oauth_token , true )
150+ end
61151 end
62152 end
63153 end
64154 end
65155 end
66156
67- error ( ' Failed to find GitHub token ' )
157+ return github_device_flow ( tag , ' Iv1.b507a08c87ecfe98 ' , ' ' )
68158end
69159
70160--- @class CopilotChat.config.providers.Options
@@ -97,7 +187,7 @@ M.copilot = {
97187 local response , err = utils .curl_get (' https://api.github.com/copilot_internal/v2/token' , {
98188 json_response = true ,
99189 headers = {
100- [' Authorization' ] = ' Token ' .. get_github_token (),
190+ [' Authorization' ] = ' Token ' .. get_github_token (' copilot ' ),
101191 },
102192 })
103193
@@ -284,57 +374,39 @@ M.copilot = {
284374}
285375
286376M .github_models = {
377+ disabled = true ,
287378 embed = ' copilot_embeddings' ,
288379
289380 get_headers = function ()
290381 return {
291- [' Authorization' ] = ' Bearer ' .. get_github_token (),
292- [' x-ms-useragent' ] = EDITOR_VERSION ,
293- [' x-ms-user-agent' ] = EDITOR_VERSION ,
382+ [' Authorization' ] = ' Bearer ' .. github_device_flow (' github_models' , ' Ov23liqtJusaUH38tIoK' , ' read:user copilot' ),
294383 }
295384 end ,
296385
297386 get_models = function (headers )
298- local response , err = utils .curl_post (' https://api.catalog.azureml.ms/asset-gallery/v1.0/models' , {
299- headers = headers ,
300- json_request = true ,
387+ local response , err = utils .curl_get (' https://models.github.ai/catalog/models' , {
301388 json_response = true ,
302- body = {
303- filters = {
304- { field = ' freePlayground' , values = { ' true' }, operator = ' eq' },
305- { field = ' labels' , values = { ' latest' }, operator = ' eq' },
306- },
307- order = {
308- { field = ' displayName' , direction = ' asc' },
309- },
310- },
389+ headers = headers ,
311390 })
312391
313392 if err then
314393 error (err )
315394 end
316395
317396 return vim
318- .iter (response .body .summaries )
319- :filter (function (model )
320- return vim .tbl_contains (model .inferenceTasks , ' chat-completion' )
321- end )
397+ .iter (response .body )
322398 :map (function (model )
323- local context_window = model .modelLimits .textLimits .inputContextWindow
324- local max_output_tokens = model .modelLimits .textLimits .maxOutputTokens
325- local max_input_tokens = context_window - max_output_tokens
326- if max_input_tokens <= 0 then
327- max_output_tokens = 4096
328- max_input_tokens = context_window - max_output_tokens
329- end
330-
399+ local max_output_tokens = model .limits .max_output_tokens
400+ local max_input_tokens = model .limits .max_input_tokens
331401 return {
332- id = model .name ,
333- name = model .displayName ,
402+ id = model .id ,
403+ name = model .name ,
334404 tokenizer = ' o200k_base' ,
335405 max_input_tokens = max_input_tokens ,
336406 max_output_tokens = max_output_tokens ,
337- streaming = true ,
407+ streaming = vim .tbl_contains (model .capabilities , ' streaming' ),
408+ tools = vim .tbl_contains (model .capabilities , ' tool-calling' ),
409+ version = model .version ,
338410 }
339411 end )
340412 :totable ()
@@ -344,7 +416,7 @@ M.github_models = {
344416 prepare_output = M .copilot .prepare_output ,
345417
346418 get_url = function ()
347- return ' https://models.inference .ai.azure.com /chat/completions'
419+ return ' https://models.github .ai/inference /chat/completions'
348420 end ,
349421}
350422
0 commit comments