From 483f71dba46c73d40dd863d926c3bbdaef83723c Mon Sep 17 00:00:00 2001 From: Aaron Pham Date: Thu, 29 Aug 2024 23:36:39 -0400 Subject: [PATCH] feat(provider): support copilot (#381) * feat(provider): add back support for copilot Signed-off-by: Aaron Pham * docs: add acknowledgement Signed-off-by: Aaron Pham --------- Signed-off-by: Aaron Pham --- README.md | 10 +- lua/avante/config.lua | 10 ++ lua/avante/providers/copilot.lua | 156 +++++++++++++++++++++++++++++++ lua/avante/providers/init.lua | 6 -- 4 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 lua/avante/providers/copilot.lua diff --git a/README.md b/README.md index 8132009..9588ff2 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ https://github.com/user-attachments/assets/86140bfd-08b4-483d-a887-1b701d9e37dd "MunifTanjim/nui.nvim", --- The below dependencies are optional, "nvim-tree/nvim-web-devicons", -- or echasnovski/mini.icons + "zbirenbaum/copilot.lua", -- for providers='copilot' { -- support for image pasting "HakonHarnes/img-clip.nvim", @@ -86,6 +87,7 @@ Plug 'MunifTanjim/nui.nvim' " Optional deps Plug 'nvim-tree/nvim-web-devicons' "or Plug 'echasnovski/mini.icons' Plug 'HakonHarnes/img-clip.nvim' +Plug 'zbirenbaum/copilot.lua' " Yay Plug 'yetone/avante.nvim' @@ -110,12 +112,14 @@ add({ }, }) --- optional +add({ source = 'zbirenbaum/copilot.lua' }) add({ source = 'HakonHarnes/img-clip.nvim' }) add({ source = 'MeanderingProgrammer/render-markdown.nvim' }) later(function() require('render-markdown').setup({...}) end) later(function() require('img-clip').setup({...}) -- config img-clip + require("copilot").setup({...}) -- setup copilot to your liking require("avante").setup({...}) -- config for avante.nvim end) @@ -134,6 +138,9 @@ end) require('img-clip').setup ({ -- use recommended settings from above }) +require('copilot').setup ({ + -- use recommended settings from above +}) require('render-markdown').setup ({ -- use recommended settings from above }) @@ -171,7 +178,7 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_ ```lua { ---@alias Provider "openai" | "claude" | "azure" | "cohere" | [string] - provider = "claude", -- Only recommend using Claude + provider = "claude", -- Recommend using Claude claude = { endpoint = "https://api.anthropic.com", model = "claude-3-5-sonnet-20240620", @@ -343,6 +350,7 @@ We would like to express our heartfelt gratitude to the contributors of the foll | [git-conflict.nvim](https://github.com/akinsho/git-conflict.nvim) | No License | Diff comparison functionality | https://github.com/yetone/avante.nvim/blob/main/lua/avante/diff.lua | | [ChatGPT.nvim](https://github.com/jackMort/ChatGPT.nvim) | Apache 2.0 License | Calculation of tokens count | https://github.com/yetone/avante.nvim/blob/main/lua/avante/utils/tokens.lua | | [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT License | Clipboard image support | https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard.lua | +| [copilot.lua](https://github.com/zbirenbaum/copilot.lua) | MIT License | Copilot support | https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/copilot.lua | The high quality and ingenuity of these projects' source code have been immensely beneficial throughout our development process. We extend our sincere thanks and respect to the authors and contributors of these projects. It is the selfless dedication of the open-source community that drives projects like avante.nvim forward. diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 1464a93..d46f7a5 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -21,6 +21,16 @@ M.defaults = { max_tokens = 4096, ["local"] = false, }, + ---@type AvanteSupportedProvider + copilot = { + endpoint = "https://api.githubcopilot.com", + model = "gpt-4o-2024-05-13", + proxy = nil, -- [protocol://]host[:port] Use this proxy + allow_insecure = false, -- Allow insecure server connections + timeout = 30000, -- Timeout in milliseconds + temperature = 0, + max_tokens = 4096, + }, ---@type AvanteAzureProvider azure = { endpoint = "", -- example: "https://.openai.azure.com" diff --git a/lua/avante/providers/copilot.lua b/lua/avante/providers/copilot.lua new file mode 100644 index 0000000..d358759 --- /dev/null +++ b/lua/avante/providers/copilot.lua @@ -0,0 +1,156 @@ +---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 O = require("avante.providers").openai + +local H = {} + +---@class OAuthToken +---@field user string +---@field oauth_token string +--- +---@return string +H.get_oauth_token = function() + local xdg_config = vim.fn.expand("$XDG_CONFIG_HOME") + local os_name = Utils.get_os_name() + ---@type string + local config_dir + + if vim.tbl_contains({ "linux", "darwin" }, os_name) then + config_dir = vim.fn.isdirectory(xdg_config) and xdg_config or vim.fn.expand("~/.config") + else + config_dir = vim.fn.expand("~/AppData/Local") + end + + local yason = Path:new(config_dir):joinpath("github-copilot", "hosts.json") + if not yason:exists() then + error("You must setup copilot with either copilot.lua or copilot.vim", 2) + end + return vim + .iter( + ---@type table + 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" +H.chat_completion_url = function(base_url) + return Utils.trim(base_url, { prefix = "/" }) .. "/chat/completions" +end + +---@class AvanteProviderFunctor +local M = {} + +H.refresh_token = function() + if not M.state then + error("internal initialization error") + end + + if + not M.state.github_token + or (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, + on_error = function(err) + error("Failed to get response: " .. vim.inspect(err)) + end, + callback = function(output) + M.state.github_token = vim.json.decode(output.body) + if not vim.g.avante_login then + vim.g.avante_login = true + end + end, + }) + end +end + +---@private +---@class AvanteCopilotState +---@field oauth_token string +---@field github_token CopilotToken? +M.state = nil + +M.api_key_name = P.AVANTE_INTERNAL_KEY + +M.parse_message = O.parse_message +M.parse_response = O.parse_response + +M.parse_curl_args = function(provider, code_opts) + H.refresh_token() + + local base, body_opts = P.parse_config(provider) + + return { + url = H.chat_completion_url(base.endpoint), + timeout = base.timeout, + proxy = base.proxy, + insecure = base.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 = base.model, + messages = M.parse_message(code_opts), + stream = true, + }, body_opts), + } +end + +M.setup = function() + if not M.state then + M.state = { github_token = nil, oauth_token = H.get_oauth_token() } + H.refresh_token() + end + vim.g.avante_login = true +end + +return M diff --git a/lua/avante/providers/init.lua b/lua/avante/providers/init.lua index 2449140..92f6aba 100644 --- a/lua/avante/providers/init.lua +++ b/lua/avante/providers/init.lua @@ -370,12 +370,6 @@ end ---@return AvanteProviderFunctor M.get_config = function(provider) provider = provider or Config.provider - if provider == "copilot" then - Utils.error( - "Sorry! We no longer support the copilot provider! Please use other providers!", - { once = true, title = "Avante" } - ) - end local cur = Config.get_provider(provider) return type(cur) == "function" and cur() or cur end