fix(copilot): refreshing copilot tokens (#935)

* fix: wait for github copilot token to refresh before calling completion

* feat: timer to refresh copilot token to prevent 401
This commit is contained in:
Sam Jones 2024-12-30 02:58:13 -04:00 committed by GitHub
parent be92be6124
commit 9abbec4c5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -35,7 +35,63 @@ local O = 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
@ -81,35 +137,59 @@ end
H.chat_auth_url = "https://api.github.com/copilot_internal/v2/token"
H.chat_completion_url = function(base_url) return Utils.url_join(base_url, "/chat/completions") end
---@class AvanteProviderFunctor
local M = {}
H.refresh_token = function()
H.refresh_token = function(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 M.state.github_token
or (M.state.github_token.expires_at and M.state.github_token.expires_at < math.floor(os.time()))
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
curl.get(H.chat_auth_url, {
headers = {
["Authorization"] = "token " .. M.state.oauth_token,
["Accept"] = "application/json",
},
timeout = Config.copilot.timeout,
proxy = Config.copilot.proxy,
insecure = Config.copilot.allow_insecure,
callback = function(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
else
error("Failed to get success response: " .. vim.inspect(response))
end
end,
})
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 handle_response = function(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
@ -139,7 +219,9 @@ end
M.parse_response = O.parse_response
M.parse_curl_args = function(provider, code_opts)
H.refresh_token()
-- 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 base, body_opts = P.parse_config(provider)
@ -162,20 +244,110 @@ M.parse_curl_args = function(provider, code_opts)
}
end
M._refresh_timer = nil
M.setup_timer = function()
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
M.setup_file_watcher = function()
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 M.state.github_token = vim.json.decode(copilot_token_file:read()) end
end)
)
end
M.setup = function()
local copilot_token_file = Path:new(copilot_path)
if not M.state then
M.state = {
github_token = copilot_token_file:exists() and vim.json.decode(copilot_token_file:read()) or nil,
oauth_token = H.get_oauth_token(),
}
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
vim.schedule(function() H.refresh_token() 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
M.cleanup = function()
-- 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
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