Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions containers/api-proxy/providers/auth-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
* integration-metadata key only requires a single edit.
*/

/**
* Build an `Authorization` header using the given scheme prefix (e.g. `Bearer`,
* `token`), optionally merged with extra headers.
*
* This is the single place that assembles an `Authorization` header value, so
* every adapter that needs a non-default prefix (e.g. Copilot's Enterprise
* `token <value>` scheme) composes it from here instead of concatenating the
* prefix and token itself.
*
* @param {string} prefix - The auth scheme prefix (e.g. `Bearer`, `token`)
* @param {string} token
* @param {Record<string, string>} [extraHeaders]
* @returns {Record<string, string>}
*/
function tokenAuthHeaders(prefix, token, extraHeaders) {
return { ...extraHeaders, 'Authorization': prefix + ' ' + token };
}

/**
* Build a `Bearer` Authorization header, optionally merged with extra headers.
*
Expand All @@ -16,7 +34,7 @@
* @returns {Record<string, string>}
*/
function bearerAuthHeaders(token, extraHeaders) {
return { ...extraHeaders, 'Authorization': 'Bearer ' + token };
return tokenAuthHeaders('Bearer', token, extraHeaders);
}

/**
Expand All @@ -43,4 +61,4 @@ function withCopilotIntegration(headers, integrationId) {
return { ...headers, 'Copilot-Integration-Id': integrationId };
}

module.exports = { bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration };
module.exports = { tokenAuthHeaders, bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration };
26 changes: 25 additions & 1 deletion containers/api-proxy/providers/auth-headers.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const { bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration } = require('./auth-headers');
const { tokenAuthHeaders, bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration } = require('./auth-headers');

describe('bearerAuthHeaders', () => {
it('builds an Authorization: Bearer ... header', () => {
Expand Down Expand Up @@ -68,3 +68,27 @@ describe('withCopilotIntegration', () => {
});
});
});

describe('tokenAuthHeaders', () => {
it('builds an Authorization header using the given prefix and token', () => {
expect(tokenAuthHeaders('token', 'gh-tok')).toEqual({ 'Authorization': 'token gh-tok' });
});


it('merges extra headers alongside the Authorization header', () => {
expect(tokenAuthHeaders('token', 'gh-tok', { 'X-GitHub-Api-Version': '2026-07-01' })).toEqual({
'X-GitHub-Api-Version': '2026-07-01',
'Authorization': 'token gh-tok',
});
});

it('does not mutate the extraHeaders argument', () => {
const extra = { 'x-custom': 'val' };
tokenAuthHeaders('token', 'gh-tok', extra);
expect(extra).toEqual({ 'x-custom': 'val' });
});

it('is the basis for bearerAuthHeaders', () => {
expect(bearerAuthHeaders('tok')).toEqual(tokenAuthHeaders('Bearer', 'tok'));
});
});
25 changes: 13 additions & 12 deletions containers/api-proxy/providers/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const {
copilotTargetRequiresGitHubTokenPrefix,
isGithubCopilotCatalogTarget,
} = require('./copilot-auth');
const { bearerAuthHeaders, withCopilotIntegration } = require('./auth-headers');
const { bearerAuthHeaders, tokenAuthHeaders, withCopilotIntegration } = require('./auth-headers');
const { URL } = require('url');
const { COPILOT_ENV } = require('../provider-env-constants');

Expand Down Expand Up @@ -124,6 +124,9 @@ function createCopilotAdapter(env, deps = {}) {
: null;
const bodyTransform = composeBodyTransforms(sanitizedBodyTransform, byokBodyFieldTransform);
const requiresGitHubTokenPrefix = copilotTargetRequiresGitHubTokenPrefix(rawTarget, env);
// The GitHub OAuth token always uses this prefix (Enterprise/Business targets
// require 'token', everything else 'Bearer'); BYOK API keys always use 'Bearer'.
const githubTokenAuthPrefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer';
const authPrefix = (requiresGitHubTokenPrefix && !apiKey) ? 'token' : 'Bearer';
// Pre-computed models path used by getModelsFetchConfig and getReflectionInfo.
// For BYOK/custom providers the base path prefix is included (e.g. /api/v1/models
Expand All @@ -142,15 +145,14 @@ function createCopilotAdapter(env, deps = {}) {
* @returns {{ url: string, opts: { method: string, headers: Record<string,string> } } & Record<string, unknown>}
*/
function buildCopilotModelsRequest(extra = {}) {
const prefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer';
return {
url: `https://${rawTarget}/models`,
opts: {
method: 'GET',
headers: withCopilotIntegration({
'Authorization': prefix + ' ' + githubToken,
'X-GitHub-Api-Version': COPILOT_MODELS_API_VERSION,
}, integrationId),
headers: withCopilotIntegration(
tokenAuthHeaders(githubTokenAuthPrefix, githubToken, { 'X-GitHub-Api-Version': COPILOT_MODELS_API_VERSION }),
integrationId
),
},
modelMetadataFormat: 'copilot',
apiVersion: COPILOT_MODELS_API_VERSION,
Expand All @@ -165,10 +167,10 @@ function createCopilotAdapter(env, deps = {}) {
env,
oidcAuthOptions: { staticAuthToken: authToken, skipWhen: !!staticAuthToken },
buildOidcHeaders: (token) => withCopilotIntegration(bearerAuthHeaders(token), integrationId),
buildStaticHeaders: () => withCopilotIntegration({
...(apiKey ? byokExtraHeaders : {}),
'Authorization': authPrefix + ' ' + authToken,
}, integrationId),
buildStaticHeaders: () => withCopilotIntegration(
tokenAuthHeaders(authPrefix, authToken, apiKey ? byokExtraHeaders : undefined),
integrationId
),
createAdapterMethodsOptions: ({ oidcConfigured, authProvider }) => ({
apiKey: authToken,
rawTarget,
Expand Down Expand Up @@ -282,8 +284,7 @@ function createCopilotAdapter(env, deps = {}) {
const isModelsPath = reqPathname === '/models' || reqPathname.startsWith('/models/');
if (isModelsPath && req.method === 'GET' && githubToken) {
// /models always uses the GitHub OAuth token (not BYOK key)
const prefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer';
return withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId);
return withCopilotIntegration(tokenAuthHeaders(githubTokenAuthPrefix, githubToken), integrationId);
}

const headers = resolveHeaders();
Expand Down
Loading