Skip to content

Commit c5aa167

Browse files
ssparrebssparreb
andauthored
feat: support dynamic *.business.githubcopilot.com base URL resolution (#1536)
Co-authored-by: ssparreb <mail@vansparreboom.com>
1 parent ecea24a commit c5aa167

File tree

1 file changed

+49
-8
lines changed

1 file changed

+49
-8
lines changed

lua/CopilotChat/config/providers.lua

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,22 @@ local function get_github_models_token(tag)
196196
return github_device_flow(tag, '178c6fc778ccc68e1d6a', 'read:user copilot')
197197
end
198198

199+
--- Resolve the Copilot API base URL from token endpoint response.
200+
--- Falls back to the default api.githubcopilot.com if no business endpoint is found.
201+
---@param token_body table The decoded JSON body from the token endpoint
202+
---@return string base_url The base URL (no trailing slash)
203+
local function resolve_copilot_base_url(token_body)
204+
-- The token response may include an `endpoints` table with an `api` field
205+
-- pointing to the correct base URL for business/enterprise accounts,
206+
-- e.g. https://api.business.githubcopilot.com
207+
if token_body and token_body.endpoints and token_body.endpoints.api then
208+
local url = token_body.endpoints.api
209+
-- Strip trailing slash if present
210+
return url:gsub('/$', '')
211+
end
212+
return 'https://api.githubcopilot.com'
213+
end
214+
199215
--- Prepare input for Responses API
200216
---@param inputs CopilotChat.client.Message[]
201217
---@param opts CopilotChat.config.providers.Options
@@ -537,12 +553,19 @@ M.copilot = {
537553
error(err)
538554
end
539555

556+
-- Resolve the base URL from the token response so that business/enterprise
557+
-- accounts using *.business.githubcopilot.com are handled automatically.
558+
local base_url = resolve_copilot_base_url(response.body)
559+
540560
return {
541561
['Authorization'] = 'Bearer ' .. response.body.token,
542562
['Editor-Version'] = EDITOR_VERSION,
543563
['Editor-Plugin-Version'] = 'CopilotChat.nvim/*',
544564
['Copilot-Integration-Id'] = 'vscode-chat',
545565
['x-github-api-version'] = '2025-10-01',
566+
-- Store the resolved base URL in a custom header so that get_models,
567+
-- resolve_model, and get_url can read it without making another request.
568+
['x-copilot-base-url'] = base_url,
546569
},
547570
response.body.expires_at
548571
end,
@@ -598,9 +621,16 @@ M.copilot = {
598621
end,
599622

600623
get_models = function(headers)
601-
local response, err = curl.get('https://api.githubcopilot.com/models', {
624+
-- Use the resolved base URL carried in the custom header, falling back to
625+
-- the default if it is absent (e.g. during tests or manual calls).
626+
local base_url = headers['x-copilot-base-url'] or 'https://api.githubcopilot.com'
627+
628+
-- Build request headers without our internal routing header.
629+
local request_headers = vim.tbl_extend('force', headers, { ['x-copilot-base-url'] = nil })
630+
631+
local response, err = curl.get(base_url .. '/models', {
602632
json_response = true,
603-
headers = headers,
633+
headers = request_headers,
604634
})
605635

606636
if err then
@@ -628,6 +658,9 @@ M.copilot = {
628658
policy = not model['policy'] or model['policy']['state'] == 'enabled',
629659
version = model.version,
630660
use_responses = use_responses,
661+
-- Carry the base URL into the model so get_url and resolve_model
662+
-- can use it without needing access to the headers again.
663+
base_url = base_url,
631664
}
632665
end)
633666
:totable()
@@ -643,8 +676,8 @@ M.copilot = {
643676

644677
for _, model in ipairs(models) do
645678
if not model.policy then
646-
pcall(curl.post, 'https://api.githubcopilot.com/models/' .. model.id .. '/policy', {
647-
headers = headers,
679+
pcall(curl.post, base_url .. '/models/' .. model.id .. '/policy', {
680+
headers = request_headers,
648681
json_request = true,
649682
body = { state = 'enabled' },
650683
})
@@ -656,6 +689,7 @@ M.copilot = {
656689
id = 'auto',
657690
name = 'Auto (Copilot)',
658691
description = 'Auto selects the best model for your request.',
692+
base_url = base_url,
659693
})
660694

661695
return models
@@ -666,9 +700,12 @@ M.copilot = {
666700
return model
667701
end
668702

669-
local url = 'https://api.githubcopilot.com/models/session'
703+
local base_url = headers['x-copilot-base-url'] or 'https://api.githubcopilot.com'
704+
local request_headers = vim.tbl_extend('force', headers, { ['x-copilot-base-url'] = nil })
705+
706+
local url = base_url .. '/models/session'
670707
local response, err = curl.post(url, {
671-
headers = headers,
708+
headers = request_headers,
672709
body = { auto_mode = { model_hints = { 'auto' } } },
673710
json_response = true,
674711
json_request = true,
@@ -707,10 +744,14 @@ M.copilot = {
707744
end,
708745

709746
get_url = function(opts)
747+
-- Use the base URL stored on the model (populated by get_models), falling
748+
-- back to the default for backwards compatibility.
749+
local base_url = (opts and opts.model and opts.model.base_url) or 'https://api.githubcopilot.com'
750+
710751
if opts and opts.model and opts.model.use_responses then
711-
return 'https://api.githubcopilot.com/responses'
752+
return base_url .. '/responses'
712753
end
713-
return 'https://api.githubcopilot.com/chat/completions'
754+
return base_url .. '/chat/completions'
714755
end,
715756
}
716757

0 commit comments

Comments
 (0)