-
Notifications
You must be signed in to change notification settings - Fork 410
/
Copy pathcopilot.lua
361 lines (306 loc) · 10.1 KB
/
copilot.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
---Reference implementation:
---https://github.com/zbirenbaum/copilot.lua/blob/master/lua/copilot/auth.lua config file
---https://github.com/zed-industries/zed/blob/ad43bbbf5eda59eba65309735472e0be58b4f7dd/crates/copilot/src/copilot_chat.rs#L272 for authorization
---
---@class CopilotToken
---@field annotations_enabled boolean
---@field chat_enabled boolean
---@field chat_jetbrains_enabled boolean
---@field code_quote_enabled boolean
---@field codesearch boolean
---@field copilotignore_enabled boolean
---@field endpoints {api: string, ["origin-tracker"]: string, proxy: string, telemetry: string}
---@field expires_at integer
---@field individual boolean
---@field nes_enabled boolean
---@field prompt_8k boolean
---@field public_suggestions string
---@field refresh_in integer
---@field sku string
---@field snippy_load_test_enabled boolean
---@field telemetry string
---@field token string
---@field tracking_id string
---@field vsc_electron_fetcher boolean
---@field xcode boolean
---@field xcode_chat boolean
local curl = require("plenary.curl")
local Config = require("avante.config")
local Path = require("plenary.path")
local Utils = require("avante.utils")
local P = require("avante.providers")
local OpenAI = require("avante.providers").openai
local H = {}
---@class AvanteProviderFunctor
local M = {}
local copilot_path = vim.fn.stdpath("data") .. "/avante/github-copilot.json"
local lockfile_path = vim.fn.stdpath("data") .. "/avante/copilot-timer.lock"
-- Lockfile management
local function is_process_running(pid)
if vim.fn.has("win32") == 1 then
return vim.fn.system('tasklist /FI "PID eq ' .. pid .. '" 2>NUL | find /I "' .. pid .. '"') ~= ""
else
return vim.fn.system("ps -p " .. pid .. " > /dev/null 2>&1; echo $?") == "0\n"
end
end
local function try_acquire_timer_lock()
local lockfile = Path:new(lockfile_path)
local tmp_lockfile = lockfile_path .. ".tmp." .. vim.fn.getpid()
Path:new(tmp_lockfile):write(tostring(vim.fn.getpid()), "w")
-- Check existing lock
if lockfile:exists() then
local content = lockfile:read()
local pid = tonumber(content)
if pid and is_process_running(pid) then
os.remove(tmp_lockfile)
return false -- Another instance is already managing
end
end
-- Attempt to take ownership
local success = os.rename(tmp_lockfile, lockfile_path)
if not success then
os.remove(tmp_lockfile)
return false
end
return true
end
local function start_manager_check_timer()
if M._manager_check_timer then
M._manager_check_timer:stop()
M._manager_check_timer:close()
end
M._manager_check_timer = vim.uv.new_timer()
M._manager_check_timer:start(
30000,
30000,
vim.schedule_wrap(function()
if not M._refresh_timer and try_acquire_timer_lock() then M.setup_timer() end
end)
)
end
---@class OAuthToken
---@field user string
---@field oauth_token string
---
---@return string
function H.get_oauth_token()
local xdg_config = vim.fn.expand("$XDG_CONFIG_HOME")
local os_name = Utils.get_os_name()
---@type string
local config_dir
if xdg_config and vim.fn.isdirectory(xdg_config) > 0 then
config_dir = xdg_config
elseif vim.tbl_contains({ "linux", "darwin" }, os_name) then
config_dir = vim.fn.expand("~/.config")
else
config_dir = vim.fn.expand("~/AppData/Local")
end
--- hosts.json (copilot.lua), apps.json (copilot.vim)
---@type Path[]
local paths = vim.iter({ "hosts.json", "apps.json" }):fold({}, function(acc, path)
local yason = Path:new(config_dir):joinpath("github-copilot", path)
if yason:exists() then table.insert(acc, yason) end
return acc
end)
if #paths == 0 then error("You must setup copilot with either copilot.lua or copilot.vim", 2) end
local yason = paths[1]
return vim
.iter(
---@type table<string, OAuthToken>
---@diagnostic disable-next-line: param-type-mismatch
vim.json.decode(yason:read())
)
:filter(function(k, _) return k:match("github.com") end)
---@param acc {oauth_token: string}
:fold({}, function(acc, _, v)
acc.oauth_token = v.oauth_token
return acc
end)
.oauth_token
end
H.chat_auth_url = "https://api.github.com/copilot_internal/v2/token"
function H.chat_completion_url(base_url) return Utils.url_join(base_url, "/chat/completions") end
function H.refresh_token(async, force)
if not M.state then error("internal initialization error") end
async = async == nil and true or async
force = force or false
-- Do not refresh token if not forced or not expired
if
not force
and M.state.github_token
and M.state.github_token.expires_at
and M.state.github_token.expires_at > math.floor(os.time())
then
return false
end
local curl_opts = {
headers = {
["Authorization"] = "token " .. M.state.oauth_token,
["Accept"] = "application/json",
},
timeout = Config.copilot.timeout,
proxy = Config.copilot.proxy,
insecure = Config.copilot.allow_insecure,
}
local function handle_response(response)
if response.status == 200 then
M.state.github_token = vim.json.decode(response.body)
local file = Path:new(copilot_path)
file:write(vim.json.encode(M.state.github_token), "w")
if not vim.g.avante_login then vim.g.avante_login = true end
-- If triggered synchronously, reset timer
if not async and M._refresh_timer then M.setup_timer() end
return true
else
error("Failed to get success response: " .. vim.inspect(response))
return false
end
end
if async then
curl.get(
H.chat_auth_url,
vim.tbl_deep_extend("force", {
callback = handle_response,
}, curl_opts)
)
else
local response = curl.get(H.chat_auth_url, curl_opts)
handle_response(response)
end
end
---@private
---@class AvanteCopilotState
---@field oauth_token string
---@field github_token CopilotToken?
M.state = nil
M.api_key_name = ""
M.tokenizer_id = "gpt-4o"
M.role_map = {
user = "user",
assistant = "assistant",
}
M.parse_messages = OpenAI.parse_messages
M.parse_response = OpenAI.parse_response
function M.parse_curl_args(provider, prompt_opts)
-- refresh token synchronously, only if it has expired
-- (this should rarely happen, as we refresh the token in the background)
H.refresh_token(false, false)
local provider_conf, request_body = P.parse_config(provider)
local disable_tools = provider_conf.disable_tools or false
local tools = {}
if not disable_tools and prompt_opts.tools then
for _, tool in ipairs(prompt_opts.tools) do
table.insert(tools, OpenAI.transform_tool(tool))
end
end
return {
url = H.chat_completion_url(provider_conf.endpoint),
timeout = provider_conf.timeout,
proxy = provider_conf.proxy,
insecure = provider_conf.allow_insecure,
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. M.state.github_token.token,
["Copilot-Integration-Id"] = "vscode-chat",
["Editor-Version"] = ("Neovim/%s.%s.%s"):format(vim.version().major, vim.version().minor, vim.version().patch),
},
body = vim.tbl_deep_extend("force", {
model = provider_conf.model,
messages = M.parse_messages(prompt_opts),
stream = true,
tools = tools,
}, request_body),
}
end
M._refresh_timer = nil
function M.setup_timer()
if M._refresh_timer then
M._refresh_timer:stop()
M._refresh_timer:close()
end
-- Calculate time until token expires
local now = math.floor(os.time())
local expires_at = M.state.github_token and M.state.github_token.expires_at or now
local time_until_expiry = math.max(0, expires_at - now)
-- Refresh 2 minutes before expiration
local initial_interval = math.max(0, (time_until_expiry - 120) * 1000)
-- Regular interval of 28 minutes after the first refresh
local repeat_interval = 28 * 60 * 1000
M._refresh_timer = vim.uv.new_timer()
M._refresh_timer:start(
initial_interval,
repeat_interval,
vim.schedule_wrap(function() H.refresh_token(true, true) end)
)
end
function M.setup_file_watcher()
if M._file_watcher then return end
local copilot_token_file = Path:new(copilot_path)
M._file_watcher = vim.uv.new_fs_event()
M._file_watcher:start(
copilot_path,
{},
vim.schedule_wrap(function()
-- Reload token from file
if copilot_token_file:exists() then
local ok, token = pcall(vim.json.decode, copilot_token_file:read())
if ok then M.state.github_token = token end
end
end)
)
end
function M.setup()
local copilot_token_file = Path:new(copilot_path)
if not M.state then M.state = {
github_token = nil,
oauth_token = H.get_oauth_token(),
} end
-- Load and validate existing token
if copilot_token_file:exists() then
local ok, token = pcall(vim.json.decode, copilot_token_file:read())
if ok and token.expires_at and token.expires_at > math.floor(os.time()) then M.state.github_token = token end
end
-- Setup timer management
local timer_lock_acquired = try_acquire_timer_lock()
if timer_lock_acquired then
M.setup_timer()
else
vim.schedule(function() H.refresh_token(true, false) end)
end
M.setup_file_watcher()
start_manager_check_timer()
require("avante.tokenizers").setup(M.tokenizer_id)
vim.g.avante_login = true
end
function M.cleanup()
-- Cleanup refresh timer
if M._refresh_timer then
M._refresh_timer:stop()
M._refresh_timer:close()
M._refresh_timer = nil
-- Remove lockfile if we were the manager
local lockfile = Path:new(lockfile_path)
if lockfile:exists() then
local content = lockfile:read()
local pid = tonumber(content)
if pid and pid == vim.fn.getpid() then lockfile:rm() end
end
end
-- Cleanup manager check timer
if M._manager_check_timer then
M._manager_check_timer:stop()
M._manager_check_timer:close()
M._manager_check_timer = nil
end
-- Cleanup file watcher
if M._file_watcher then
---@diagnostic disable-next-line: param-type-mismatch
M._file_watcher:stop()
M._file_watcher = nil
end
end
-- Register cleanup on Neovim exit
vim.api.nvim_create_autocmd("VimLeavePre", {
callback = function() M.cleanup() end,
})
return M