From 217771829526dc4cd1df0efb99057084d7a83b0c Mon Sep 17 00:00:00 2001 From: Morten Hersson Date: Sun, 3 Aug 2025 18:25:53 +0200 Subject: [PATCH] Add support for ghe.com endpoints --- README.md | 16 +++++ lua/CopilotChat/client.lua | 8 ++- lua/CopilotChat/config.lua | 9 +++ lua/CopilotChat/config/providers.lua | 87 ++++++++++++++++++++++++---- lua/CopilotChat/init.lua | 5 ++ 5 files changed, 112 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6e923106..3e4d9508 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,22 @@ Most users only need to configure a few options: } ``` +## GitHub Enterprise Configuration + +For GitHub Enterprise (GHE) users, you can configure custom endpoints: + +```lua +{ + api_endpoints = { + api_url = 'https://copilot-api.your-domain.ghe.com/chat/completions', + auth_url = 'https://api.your-domain.ghe.com/copilot_internal/v2/token', + models_url = 'https://copilot-api.your-domain.ghe.com/models' + }, +} +``` + +Replace `your-domain` with your actual GHE server domain. If not specified, the plugin defaults to GitHub's public servers. + ## Buffer Behavior ```lua diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 93e1c91d..3413699c 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -192,8 +192,11 @@ function Client:get_providers(supported_method) for _, provider_name in ipairs(provider_names) do local provider = providers[provider_name] - if provider and not provider.disabled and (not supported_method or provider[supported_method]) then - out:set(provider_name, provider) + -- Filter out non-provider entries (functions starting with _) + if provider and type(provider) == 'table' and not provider_name:match('^_') then + if not provider.disabled and (not supported_method or provider[supported_method]) then + out:set(provider_name, provider) + end end end return out @@ -605,5 +608,6 @@ function Client:running() return self.current_job ~= nil end + --- @type CopilotChat.client.Client return Client() diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b78ddf61..1dbca45c 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -53,6 +53,7 @@ ---@field functions table? ---@field prompts table? ---@field mappings CopilotChat.config.mappings? +---@field api_endpoints table? return { -- Shared config starts here (can be passed to functions at runtime and configured via setup function) @@ -130,4 +131,12 @@ return { -- default mappings mappings = require('CopilotChat.config.mappings'), + + -- Custom API endpoints configuration (optional, e.g. for GitHub Enterprise) + api_endpoints = nil, -- Set to configure custom endpoints, e.g.: + -- api_endpoints = { + -- api_url = 'https://copilot-api.your-domain.ghe.com/chat/completions', + -- auth_url = 'https://api.your-domain.ghe.com/copilot_internal/v2/token', + -- models_url = 'https://copilot-api.your-domain.ghe.com/models' + -- }, } diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5b4e95f9..81f65d21 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -5,9 +5,13 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') local files = require('CopilotChat.utils.files') +local curl = require('CopilotChat.utils.curl') local EDITOR_VERSION = 'Neovim/' .. vim.version().major .. '.' .. vim.version().minor .. '.' .. vim.version().patch +-- Store configuration for GHE endpoints +local _ghe_config = {} + local token_cache = nil local unsaved_token_cache = {} local function load_tokens() @@ -57,7 +61,11 @@ end ---@return string local function github_device_flow(tag, client_id, scope) local function request_device_code() - local res = curl.post('https://github.com/login/device/code', { + local config = _ghe_config + local auth_url = config.auth_url or 'https://api.github.com/copilot_internal/v2/token' + local github_url = auth_url:match('https://api%.(.-)%.com') or 'github' + local device_code_url = 'https://' .. github_url .. '.com/login/device/code' + local res = curl.post(device_code_url, { body = { client_id = client_id, scope = scope, @@ -72,7 +80,11 @@ local function github_device_flow(tag, client_id, scope) local function poll_for_token(device_code, interval) plenary_utils.sleep(interval * 1000) - local res = curl.post('https://github.com/login/oauth/access_token', { + local config = _ghe_config + local auth_url = config.auth_url or 'https://api.github.com/copilot_internal/v2/token' + local github_url = auth_url:match('https://api%.(.-)%.com') or 'github' + local access_token_url = 'https://' .. github_url .. '.com/login/oauth/access_token' + local res = curl.post(access_token_url, { json_response = true, body = { client_id = client_id, @@ -157,7 +169,10 @@ local function get_github_copilot_token(tag) local parsed_data = utils.json_decode(file_data) if parsed_data then for key, value in pairs(parsed_data) do - if string.find(key, 'github.com') and value and value.oauth_token then + local config = _ghe_config + local auth_url = config.auth_url or 'https://api.github.com/copilot_internal/v2/token' + local github_host = auth_url:match('https://api%.(.-)%.com') or 'github' + if string.find(key, github_host .. '.com') and value and value.oauth_token then return set_token(tag, value.oauth_token, false) end end @@ -184,7 +199,10 @@ local function get_github_models_token(tag) -- loading token from gh cli if available if vim.fn.executable('gh') == 0 then - local result = utils.system({ 'gh', 'auth', 'token', '-h', 'github.com' }) + local config = _ghe_config + local auth_url = config.auth_url or 'https://api.github.com/copilot_internal/v2/token' + local github_host = auth_url:match('https://api%.(.-)%.com') or 'github' + local result = utils.system({ 'gh', 'auth', 'token', '-h', github_host .. '.com' }) if result and result.code == 0 and result.stdout then local gh_token = vim.trim(result.stdout) if gh_token ~= '' and not gh_token:find('no oauth token') then @@ -250,9 +268,19 @@ end ---@type table local M = {} +-- Function to set GHE config (private, not a provider) +local function set_ghe_config(config) + _ghe_config = config or {} +end + +-- Export the function without adding it to the providers table +M._set_ghe_config = set_ghe_config + M.copilot = { get_headers = function() - local response, err = curl.get('https://api.github.com/copilot_internal/v2/token', { + local config = _ghe_config + local auth_url = config.auth_url or 'https://api.github.com/copilot_internal/v2/token' + local response, err = curl.get(auth_url, { json_response = true, headers = { ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), @@ -272,8 +300,11 @@ M.copilot = { response.body.expires_at end, - get_info = function() - local response, err = curl.get('https://api.github.com/copilot_internal/user', { + get_info = function(headers) + local config = _ghe_config + local auth_url = config.auth_url or 'https://api.github.com/copilot_internal/v2/token' + local base_url = auth_url:gsub('/copilot_internal/v2/token$', '') + local response, err = curl.get(base_url .. '/copilot_internal/user', { json_response = true, headers = { ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), @@ -323,7 +354,9 @@ M.copilot = { end, get_models = function(headers) - local response, err = curl.get('https://api.githubcopilot.com/models', { + local config = _ghe_config + local models_url = config.models_url or 'https://api.githubcopilot.com/models' + local response, err = curl.get(models_url, { json_response = true, headers = headers, }) @@ -368,7 +401,9 @@ M.copilot = { for _, model in ipairs(models) do if not model.policy then - pcall(curl.post, 'https://api.githubcopilot.com/models/' .. model.id .. '/policy', { + local config = _ghe_config + local models_url = config.models_url or 'https://api.githubcopilot.com/models' + curl.post(models_url:gsub('/models$', '') .. '/models/' .. model.id .. '/policy', { headers = headers, json_request = true, body = { state = 'enabled' }, @@ -723,13 +758,17 @@ M.copilot = { end, get_url = function(opts) + local config = _ghe_config + local base_api_url = config.api_url or 'https://api.githubcopilot.com/chat/completions' + -- Check if this model uses the Responses API if opts and opts.model and opts.model.use_responses then - return 'https://api.githubcopilot.com/responses' + -- Derive responses URL from chat completions URL + return base_api_url:gsub('/chat/completions$', '/responses') end -- Default to Chat Completion API - return 'https://api.githubcopilot.com/chat/completions' + return base_api_url end, } @@ -780,4 +819,30 @@ M.github_models = { end, } +M.copilot_embeddings = { + get_headers = M.copilot.get_headers, + + embed = function(inputs, headers) + local config = _ghe_config + local api_url = config.api_url or 'https://api.githubcopilot.com/chat/completions' + local embeddings_url = api_url:gsub('/chat/completions$', '/embeddings') + local response, err = curl.post(embeddings_url, { + headers = headers, + json_request = true, + json_response = true, + body = { + dimensions = 512, + input = inputs, + model = 'text-embedding-3-small', + }, + }) + + if err then + error(err) + end + + return response.body.data + end, +} + return M diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9125bb4a..a3fb10a7 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -689,6 +689,11 @@ function M.setup(config) proxy = M.config.proxy, }) + -- Pass API endpoints configuration to providers if available + if M.config.api_endpoints then + M.config.providers._set_ghe_config(M.config.api_endpoints) + end + -- Load the providers client:stop() client:set_providers(function()