diff --git a/README.md b/README.md index 7958837a..19e790c6 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,9 @@ Custom providers can implement these methods: -- Optional: Embeddings provider name or function embed?: string|function, + -- Optional: Extra info about the provider displayed in info panel + get_info?(): string[] + -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index b0a48b85..11c353b6 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -284,6 +284,37 @@ function Client:models() return self.model_cache end +--- Get information about all providers +---@return table +function Client:info() + local infos = {} + local now = math.floor(os.time()) + local CACHE_TTL = 300 -- 5 minutes + + for provider_name, provider in pairs(self.providers) do + if not provider.disabled and provider.get_info then + local cache = self.provider_cache[provider_name] + if cache and cache.info and cache.info_expires_at and cache.info_expires_at > now then + infos[provider_name] = cache.info + else + local ok, info = pcall(provider.get_info, self:authenticate(provider_name)) + if ok then + infos[provider_name] = info + if cache then + cache.info = info + cache.info_expires_at = now + CACHE_TTL + end + else + log.warn('Failed to get info for provider ' .. provider_name .. ': ' .. info) + end + end + end + end + + log.debug('Fetched provider infos:', #vim.tbl_keys(infos)) + return infos +end + --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.client.AskOptions: Options for the request diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index bdcb93ce..73cb25a0 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,5 +1,6 @@ local async = require('plenary.async') local copilot = require('CopilotChat') +local client = require('CopilotChat.client') local utils = require('CopilotChat.utils') ---@class CopilotChat.config.mappings.Diff @@ -439,6 +440,7 @@ return { local system_prompt = config.system_prompt async.run(function() + local infos = client:info() local selected_model = copilot.resolve_model(prompt, config) local selected_tools, resolved_resources = copilot.resolve_functions(prompt, config) selected_tools = vim.tbl_map(function(tool) @@ -451,6 +453,14 @@ return { table.insert(lines, '**Temp Files**: `' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`') table.insert(lines, '') + for provider, infolines in pairs(infos) do + table.insert(lines, '**Provider**: `' .. provider .. '`') + for _, line in ipairs(infolines) do + table.insert(lines, line) + end + table.insert(lines, '') + end + if source and utils.buf_valid(source.bufnr) then local source_name = vim.api.nvim_buf_get_name(source.bufnr) table.insert(lines, '**Source**: `' .. source_name .. '`') diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index eee655a8..5b35a2b4 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -171,6 +171,7 @@ end ---@class CopilotChat.config.providers.Provider ---@field disabled nil|boolean ---@field get_headers nil|fun():table,number? +---@field get_info nil|fun(headers:table):string[] ---@field get_models nil|fun(headers:table):table ---@field embed nil|string|fun(inputs:table, headers:table):table ---@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table @@ -204,6 +205,56 @@ M.copilot = { response.body.expires_at end, + get_info = function(headers) + local response, err = utils.curl_get('https://api.github.com/copilot_internal/user', { + json_response = true, + headers = { + ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + }, + }) + + if err then + error(err) + end + + local stats = response.body + local lines = {} + + if not stats or not stats.quota_snapshots then + return { 'No Copilot stats available.' } + end + + local function usage_line(name, snap) + if not snap then + return + end + + table.insert(lines, string.format(' **%s**', name)) + + if snap.unlimited then + table.insert(lines, ' Usage: Unlimited') + else + local used = snap.entitlement - snap.remaining + local percent = snap.entitlement > 0 and (used / snap.entitlement * 100) or 0 + table.insert(lines, string.format(' Usage: %d / %d (%.1f%%)', used, snap.entitlement, percent)) + table.insert(lines, string.format(' Remaining: %d', snap.remaining)) + if snap.overage_permitted ~= nil then + table.insert(lines, ' Overage: ' .. (snap.overage_permitted and 'Permitted' or 'Not Permitted')) + end + end + end + + usage_line('Premium requests', stats.quota_snapshots.premium_interactions) + usage_line('Chat', stats.quota_snapshots.chat) + usage_line('Completions', stats.quota_snapshots.completions) + + if stats.quota_reset_date then + table.insert(lines, string.format(' **Quota** resets on: %s', stats.quota_reset_date)) + end + + return lines + end, + get_models = function(headers) local response, err = utils.curl_get('https://api.githubcopilot.com/models', { json_response = true,