diff --git a/containers/api-proxy/adapter-factory.js b/containers/api-proxy/adapter-factory.js index 3cd7a1f65..89a46750d 100644 --- a/containers/api-proxy/adapter-factory.js +++ b/containers/api-proxy/adapter-factory.js @@ -211,6 +211,73 @@ function createProviderAuthScaffold(env, deps = {}, { keyEnvVar, targetEnvVar, b return { apiKey, rawTarget, basePath, bodyTransform }; } +/** + * Create an OIDC-aware adapter using the shared auth/header/runtime scaffold. + * + * @param {object} opts + * @param {Record} opts.env + * @param {object} [opts.oidcAuthOptions] + * @param {(token: string) => Record} opts.buildOidcHeaders + * @param {() => Record} opts.buildStaticHeaders + * @param {object|((ctx: object) => object)} opts.createAdapterMethodsOptions + * @param {object|((ctx: object) => object)} opts.buildAdapterOptions + * @param {(ctx: object) => Record} [opts.getAuthHeaders] + * @returns {import('./providers/index').ProviderAdapter} + */ +function createOidcAwareProviderAdapter({ + env, + oidcAuthOptions = {}, + buildOidcHeaders, + buildStaticHeaders, + createAdapterMethodsOptions, + buildAdapterOptions, + getAuthHeaders, +}) { + const { createProviderOidcHeaderStrategy } = require('./providers/cloud-oidc-init'); + const oidc = createProviderOidcHeaderStrategy(env, oidcAuthOptions, { + buildOidcHeaders, + buildStaticHeaders, + }); + const { + authProvider, + oidcProvider, + awsOidcProvider, + oidcConfigured, + runtimeMethods, + validationSkip, + skipModelsFetch, + resolveHeaders, + } = oidc; + const context = { + authProvider, + oidcProvider, + awsOidcProvider, + oidcConfigured, + runtimeMethods, + validationSkip, + skipModelsFetch, + resolveHeaders, + }; + const adapterMethods = createAdapterMethods(typeof createAdapterMethodsOptions === 'function' + ? createAdapterMethodsOptions(context) + : createAdapterMethodsOptions); + const adapterOptions = typeof buildAdapterOptions === 'function' + ? buildAdapterOptions({ ...context, adapterMethods }) + : buildAdapterOptions; + + return buildProviderAdapter({ + ...adapterOptions, + adapterMethods, + getAuthHeaders: getAuthHeaders + ? (req) => getAuthHeaders({ ...context, req }) + : (() => resolveHeaders()), + extra: { + ...runtimeMethods, + ...(adapterOptions.extra || {}), + }, + }); +} + /** * Assemble a provider adapter object from its constituent parts. * @@ -338,6 +405,7 @@ function buildProviderAdapter({ module.exports = { createBaseAdapterConfig, createProviderAuthScaffold, + createOidcAwareProviderAdapter, createAdapterMethods, buildProviderAdapter, }; diff --git a/containers/api-proxy/adapter-factory.test.js b/containers/api-proxy/adapter-factory.test.js index 6670d6558..b9a87aee2 100644 --- a/containers/api-proxy/adapter-factory.test.js +++ b/containers/api-proxy/adapter-factory.test.js @@ -505,3 +505,71 @@ describe('createProviderAuthScaffold', () => { expect(result.rawTarget).toBe('custom.example.com'); }); }); + +describe('createOidcAwareProviderAdapter', () => { + const { createOidcAwareProviderAdapter } = require('./adapter-factory'); + + it('wires static auth headers and runtime methods into a provider adapter', () => { + const adapter = createOidcAwareProviderAdapter({ + env: {}, + oidcAuthOptions: { staticAuthToken: 'static-key' }, + buildOidcHeaders: (token) => ({ Authorization: ['Bearer', token].join(' ') }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + createAdapterMethodsOptions: ({ validationSkip, skipModelsFetch }) => ({ + apiKey: 'static-key', + rawTarget: 'api.example.com', + basePath: '', + provider: 'test', + port: 10099, + modelsPath: '/v1/models', + validationPath: '/v1/models', + validationHeaders: () => ({ 'x-api-key': 'static-key' }), + validationSkip, + skipModelsFetch, + }), + buildAdapterOptions: { + name: 'test', + port: 10099, + isManagementPort: false, + }, + }); + + expect(adapter.isEnabled()).toBe(true); + expect(adapter.getAuthHeaders()).toEqual({ 'x-api-key': 'static-key' }); + expect(adapter.getTargetHost()).toBe('api.example.com'); + }); + + it('uses custom getAuthHeaders and exposes OIDC runtime methods when configured', () => { + const adapter = createOidcAwareProviderAdapter({ + env: {}, + oidcAuthOptions: { + oidcProviderFactory: () => ({ isReady: () => true, getToken: () => 'oidc-token' }), + }, + buildOidcHeaders: (token) => ({ Authorization: ['Bearer', token].join(' ') }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + createAdapterMethodsOptions: ({ oidcConfigured }) => ({ + apiKey: undefined, + rawTarget: 'api.example.com', + basePath: '', + provider: 'test', + port: 10099, + modelsPath: '/v1/models', + reflectionConfigured: oidcConfigured, + }), + buildAdapterOptions: { + name: 'test', + port: 10099, + isManagementPort: false, + }, + getAuthHeaders: ({ resolveHeaders }) => ({ + ...resolveHeaders(), + 'x-extra': '1', + }), + }); + + expect(adapter.isEnabled()).toBe(true); + const headers = adapter.getAuthHeaders(); + expect(headers['x-extra']).toBe('1'); + expect(typeof headers.Authorization).toBe('string'); + }); +}); diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 03924c06b..727adba7d 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -18,10 +18,9 @@ const { const { validateAuthHeaderEnv, } = require('../oidc-adapter-utils'); -const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); +const { createProviderAuthScaffold, createOidcAwareProviderAdapter } = require('../adapter-factory'); const { AnthropicOidcTokenProvider } = require('../anthropic-oidc-token-provider'); const { ANTHROPIC_ENV } = require('../provider-env-constants'); -const { createProviderOidcHeaderStrategy } = require('./cloud-oidc-init'); const { bearerAuthHeaders, providerKeyHeaders } = require('./auth-headers'); let makeAnthropicTransform, loadCustomTransform, EXTENDED_CACHE_BETA; @@ -60,38 +59,6 @@ function createAnthropicAdapter(env, deps = {}) { const oidcRequested = (env.AWF_AUTH_TYPE || '').trim().toLowerCase() === 'github-oidc' && (env.AWF_AUTH_PROVIDER || '').trim().toLowerCase() === 'anthropic'; - const { - oidcProvider, oidcConfigured, - runtimeMethods: oidcRuntimeMethods, - resolveHeaders: resolveOidcHeaders, - } = createProviderOidcHeaderStrategy(env, { - staticAuthToken: apiKey, - oidcProviderFactory: oidcRequested ? (env) => { - const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL; - const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - if (!requestUrl || !requestToken) return null; - const workspaceId = env.AWF_AUTH_ANTHROPIC_WORKSPACE_ID; - const tokenEndpoint = (env.AWF_AUTH_ANTHROPIC_TOKEN_URL || '').trim(); - return new AnthropicOidcTokenProvider({ - requestUrl, - requestToken, - federationRuleId: env.AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID, - organizationId: env.AWF_AUTH_ANTHROPIC_ORGANIZATION_ID, - serviceAccountId: env.AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID, - ...(workspaceId !== undefined ? { workspaceId } : {}), - ...(tokenEndpoint ? { tokenEndpoint } : {}), - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'https://api.anthropic.com', - }); - } : null, - }, { - buildOidcHeaders: (token) => bearerAuthHeaders(token), - buildStaticHeaders: () => providerKeyHeaders(authHeaderName, apiKey), - }); - - const oidcUnavailableError = oidcConfigured - ? 'Anthropic OIDC token unavailable; retry shortly' - : 'Anthropic OIDC requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN (permissions: id-token: write).'; - // ── Anthropic-specific optimisations ────────────────────────────────────── const autoCache = (env.AWF_ANTHROPIC_AUTO_CACHE === '1' || env.AWF_ANTHROPIC_AUTO_CACHE === 'true'); const cacheTailTtl = (() => { @@ -117,54 +84,107 @@ function createAnthropicAdapter(env, deps = {}) { // Build the composed transform once at construction time to avoid // re-allocating the wrapper function on every request. const composedBodyTransform = composeBodyTransforms(depsBodyTransform, optimisationsTransform); - const adapterMethods = createAdapterMethods({ - apiKey, - rawTarget, - basePath, - provider: 'anthropic', - port: 10001, - defaultTarget: 'api.anthropic.com', - validationPath: '/v1/messages', - validationMethod: 'POST', - validationBody: '{}', - validationHeaders: () => ({ - ...resolveOidcHeaders(), - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }), - validationSkip: () => { - if (!oidcConfigured) return null; - // After OIDC init, validate using the acquired token - if (oidcProvider.isReady()) return null; - return { skip: true, reason: 'OIDC auth; token not yet available' }; + return createOidcAwareProviderAdapter({ + env, + oidcAuthOptions: { + staticAuthToken: apiKey, + oidcProviderFactory: oidcRequested ? (env) => { + const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL; + const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + if (!requestUrl || !requestToken) return null; + const workspaceId = env.AWF_AUTH_ANTHROPIC_WORKSPACE_ID; + const tokenEndpoint = (env.AWF_AUTH_ANTHROPIC_TOKEN_URL || '').trim(); + return new AnthropicOidcTokenProvider({ + requestUrl, + requestToken, + federationRuleId: env.AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID, + organizationId: env.AWF_AUTH_ANTHROPIC_ORGANIZATION_ID, + serviceAccountId: env.AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID, + ...(workspaceId !== undefined ? { workspaceId } : {}), + ...(tokenEndpoint ? { tokenEndpoint } : {}), + oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'https://api.anthropic.com', + }); + } : null, }, - skipModelsFetch: () => oidcConfigured && !oidcProvider?.isReady(), - modelsPath: '/v1/models', - modelsFetchHeaders: () => ({ - ...resolveOidcHeaders(), - 'anthropic-version': '2023-06-01', - }), - reflectionConfigured: !!apiKey || oidcRequested, - reflectionExtra: () => ({ - auth_type: oidcRequested ? 'github-oidc/anthropic' : 'static-key', + buildOidcHeaders: (token) => bearerAuthHeaders(token), + buildStaticHeaders: () => providerKeyHeaders(authHeaderName, apiKey), + createAdapterMethodsOptions: ({ oidcConfigured, oidcProvider, resolveHeaders }) => ({ + apiKey, + rawTarget, + basePath, + provider: 'anthropic', + port: 10001, + defaultTarget: 'api.anthropic.com', + validationPath: '/v1/messages', + validationMethod: 'POST', + validationBody: '{}', + validationHeaders: () => ({ + ...resolveHeaders(), + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }), + validationSkip: () => { + if (!oidcConfigured) return null; + // After OIDC init, validate using the acquired token + if (oidcProvider.isReady()) return null; + return { skip: true, reason: 'OIDC auth; token not yet available' }; + }, + skipModelsFetch: () => oidcConfigured && !oidcProvider?.isReady(), + modelsPath: '/v1/models', + modelsFetchHeaders: () => ({ + ...resolveHeaders(), + 'anthropic-version': '2023-06-01', + }), + reflectionConfigured: !!apiKey || oidcRequested, + reflectionExtra: () => ({ + auth_type: oidcRequested ? 'github-oidc/anthropic' : 'static-key', + }), }), - }); - - return buildProviderAdapter({ - name: 'anthropic', - port: 10001, - isManagementPort: false, - adapterMethods, + buildAdapterOptions: ({ oidcConfigured }) => { + const oidcUnavailableError = oidcConfigured + ? 'Anthropic OIDC token unavailable; retry shortly' + : 'Anthropic OIDC requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN (permissions: id-token: write).'; + return { + name: 'anthropic', + port: 10001, + isManagementPort: false, + bodyTransform: composedBodyTransform, + missingCredentialResponse: { + kind: 'provider_not_configured', + message: 'Credentials for Anthropic (port 10001) are not configured. Set ANTHROPIC_API_KEY to enable this provider.', + }, + unconfiguredResponseWhen: () => (oidcRequested + ? { + kind: 'plain_error', + statusCode: 503, + message: oidcUnavailableError, + } + : null), + healthServiceName: 'awf-api-proxy-anthropic', + missingCredentialMessage: 'ANTHROPIC_API_KEY not configured in api-proxy sidecar', + unavailableWhen: () => oidcRequested ? { message: oidcUnavailableError, status: 'unavailable' } : null, + extra: { + // Exposed for introspection (logging, tests) + _autoCache: autoCache, + _cacheTailTtl: cacheTailTtl, + _dropTools: dropTools, + _stripAnsi: stripAnsi, + _transformFile: transformFile, + _customTransformLoaded: !!customTransform, + _optimisationsTransform: optimisationsTransform, + }, + }; + }, /** * Build Anthropic auth headers for this request. * Merges in the anthropic-version default and anthropic-beta (for auto-cache) * as needed, without overwriting values already set by the client. * - * @param {import('http').IncomingMessage} req + * @param {{ resolveHeaders: () => Record, req: import('http').IncomingMessage }} params * @returns {Record} */ - getAuthHeaders(req) { - const headers = resolveOidcHeaders(); + getAuthHeaders({ resolveHeaders, req }) { + const headers = resolveHeaders(); // OIDC configured but token not yet ready: fail closed so static creds are not leaked. if (Object.keys(headers).length === 0) { @@ -191,32 +211,6 @@ function createAnthropicAdapter(env, deps = {}) { return mergedHeaders; }, - bodyTransform: composedBodyTransform, - missingCredentialResponse: { - kind: 'provider_not_configured', - message: 'Credentials for Anthropic (port 10001) are not configured. Set ANTHROPIC_API_KEY to enable this provider.', - }, - unconfiguredResponseWhen: () => (oidcRequested - ? { - kind: 'plain_error', - statusCode: 503, - message: oidcUnavailableError, - } - : null), - healthServiceName: 'awf-api-proxy-anthropic', - missingCredentialMessage: 'ANTHROPIC_API_KEY not configured in api-proxy sidecar', - unavailableWhen: () => oidcRequested ? { message: oidcUnavailableError, status: 'unavailable' } : null, - extra: { - ...oidcRuntimeMethods, - // Exposed for introspection (logging, tests) - _autoCache: autoCache, - _cacheTailTtl: cacheTailTtl, - _dropTools: dropTools, - _stripAnsi: stripAnsi, - _transformFile: transformFile, - _customTransformLoaded: !!customTransform, - _optimisationsTransform: optimisationsTransform, - }, }); } diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index 432f3f4fa..ca387352c 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -21,7 +21,7 @@ const { normalizeBasePath, composeBodyTransforms, } = require('../proxy-utils'); -const { createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); +const { createOidcAwareProviderAdapter } = require('../adapter-factory'); const { sanitizeNullToolCallTypes } = require('../body-transform'); const { parseByokExtraHeaders, @@ -35,7 +35,6 @@ const { deriveCopilotApiTarget, copilotTargetRequiresGitHubTokenPrefix, } = require('./copilot-auth'); -const { createProviderOidcHeaderStrategy } = require('./cloud-oidc-init'); const { bearerAuthHeaders, withCopilotIntegration } = require('./auth-headers'); const { URL } = require('url'); const { COPILOT_ENV } = require('../provider-env-constants'); @@ -99,18 +98,6 @@ function createCopilotAdapter(env, deps = {}) { const bodyTransform = composeBodyTransforms(sanitizedBodyTransform, byokBodyFieldTransform); const requiresGitHubTokenPrefix = copilotTargetRequiresGitHubTokenPrefix(rawTarget, env); const authPrefix = (requiresGitHubTokenPrefix && !apiKey) ? 'token' : 'Bearer'; - const { - authProvider, oidcProvider, awsOidcProvider, oidcConfigured, - runtimeMethods: oidcRuntimeMethods, - resolveHeaders: resolveInferenceHeaders, - } = createProviderOidcHeaderStrategy(env, { staticAuthToken: authToken, skipWhen: !!staticAuthToken }, { - buildOidcHeaders: (token) => withCopilotIntegration(bearerAuthHeaders(token), integrationId), - buildStaticHeaders: () => withCopilotIntegration({ - ...(apiKey ? byokExtraHeaders : {}), - 'Authorization': authPrefix + ' ' + authToken, - }, integrationId), - }); - // Pre-computed models path used by getModelsFetchConfig and getReflectionInfo. // For BYOK/custom providers the base path prefix is included (e.g. /api/v1/models // for COPILOT_PROVIDER_BASE_URL=https://openrouter.ai/api/v1). @@ -127,96 +114,127 @@ function createCopilotAdapter(env, deps = {}) { * to merge into the returned object (e.g. `{ cacheKey: 'copilot' }`). * @returns {{ url: string, opts: { method: string, headers: Record } } & Record} */ -function buildCopilotModelsRequest(extra = {}) { - const prefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer'; - return { - url: `https://${rawTarget}/models`, - opts: { - method: 'GET', - headers: withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId), - }, - ...extra, - }; -} + function buildCopilotModelsRequest(extra = {}) { + const prefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer'; + return { + url: `https://${rawTarget}/models`, + opts: { + method: 'GET', + headers: withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId), + }, + ...extra, + }; + } // Copilot has dual auth modes (GitHub OAuth vs BYOK) with different validation // and model-fetch rules, so we override those two methods while still sharing // the common reflection method shape from createAdapterMethods. - const adapterMethods = createAdapterMethods({ - apiKey: authToken, - rawTarget, - basePath, - provider: 'copilot', - port: 10002, - modelsPath, - reflectionConfigured: !!authToken || oidcConfigured, - reflectionModelsPath: modelsPath, - getValidationProbe() { - if (oidcConfigured) { - return { skip: true, reason: `OIDC auth (${authProvider}); validation via token acquisition` }; - } - if (!authToken) return null; - - // Only COPILOT_GITHUB_TOKEN has a probe endpoint (/models). - // COPILOT_PROVIDER_API_KEY alone cannot be validated at startup. - if (!githubToken) { - return { - skip: true, - reason: 'COPILOT_PROVIDER_API_KEY configured but startup validation is not supported for this auth mode', - }; - } + return createOidcAwareProviderAdapter({ + env, + oidcAuthOptions: { staticAuthToken: authToken, skipWhen: !!staticAuthToken }, + buildOidcHeaders: (token) => withCopilotIntegration(bearerAuthHeaders(token), integrationId), + buildStaticHeaders: () => withCopilotIntegration({ + ...(apiKey ? byokExtraHeaders : {}), + 'Authorization': authPrefix + ' ' + authToken, + }, integrationId), + createAdapterMethodsOptions: ({ oidcConfigured, authProvider }) => ({ + apiKey: authToken, + rawTarget, + basePath, + provider: 'copilot', + port: 10002, + modelsPath, + reflectionConfigured: !!authToken || oidcConfigured, + reflectionModelsPath: modelsPath, + getValidationProbe() { + if (oidcConfigured) { + return { skip: true, reason: `OIDC auth (${authProvider}); validation via token acquisition` }; + } + if (!authToken) return null; - if (rawTarget !== 'api.githubcopilot.com') { - return { skip: true, reason: `Custom target ${rawTarget}; validation skipped` }; - } + // Only COPILOT_GITHUB_TOKEN has a probe endpoint (/models). + // COPILOT_PROVIDER_API_KEY alone cannot be validated at startup. + if (!githubToken) { + return { + skip: true, + reason: 'COPILOT_PROVIDER_API_KEY configured but startup validation is not supported for this auth mode', + }; + } - return buildCopilotModelsRequest(); - }, - getModelsFetchConfig() { - // OIDC mode: skip startup model fetch — the token isn't available yet at this - // point and the upstream BYOK target typically isn't api.githubcopilot.com. - if (oidcConfigured) return null; - if (!authToken) return null; + if (rawTarget !== 'api.githubcopilot.com') { + return { skip: true, reason: `Custom target ${rawTarget}; validation skipped` }; + } - // Standard Copilot API (api.githubcopilot.com): - // The /models endpoint only accepts GitHub OAuth tokens (COPILOT_GITHUB_TOKEN). - // Skip startup model fetch when only a BYOK API key is configured. - if (rawTarget === 'api.githubcopilot.com') { - if (!githubToken) return null; - return buildCopilotModelsRequest({ cacheKey: 'copilot' }); - } + return buildCopilotModelsRequest(); + }, + getModelsFetchConfig() { + // OIDC mode: skip startup model fetch — the token isn't available yet at this + // point and the upstream BYOK target typically isn't api.githubcopilot.com. + if (oidcConfigured) return null; + if (!authToken) return null; - // BYOK / custom provider (e.g. OpenRouter): - // Use the explicit BYOK API key (COPILOT_PROVIDER_API_KEY) rather than authToken - // to ensure we never send a GitHub OAuth token to third-party providers. - // Skip the fetch when no BYOK key is configured. - if (!apiKey) return null; - return { - url: `https://${rawTarget}${modelsPath}`, - opts: { - method: 'GET', - headers: bearerAuthHeaders(apiKey), - }, - cacheKey: 'copilot', - }; - }, - }); + // Standard Copilot API (api.githubcopilot.com): + // The /models endpoint only accepts GitHub OAuth tokens (COPILOT_GITHUB_TOKEN). + // Skip startup model fetch when only a BYOK API key is configured. + if (rawTarget === 'api.githubcopilot.com') { + if (!githubToken) return null; + return buildCopilotModelsRequest({ cacheKey: 'copilot' }); + } - return buildProviderAdapter({ - name: 'copilot', - port: 10002, - isManagementPort: false, - adapterMethods, + // BYOK / custom provider (e.g. OpenRouter): + // Use the explicit BYOK API key (COPILOT_PROVIDER_API_KEY) rather than authToken + // to ensure we never send a GitHub OAuth token to third-party providers. + // Skip the fetch when no BYOK key is configured. + if (!apiKey) return null; + return { + url: `https://${rawTarget}${modelsPath}`, + opts: { + method: 'GET', + headers: bearerAuthHeaders(apiKey), + }, + cacheKey: 'copilot', + }; + }, + }), + buildAdapterOptions: ({ authProvider, oidcConfigured, oidcProvider, awsOidcProvider }) => ({ + name: 'copilot', + port: 10002, + isManagementPort: false, + bodyTransform, + missingCredentialResponse: { + kind: 'provider_not_configured', + message: 'Credentials for GitHub Copilot (port 10002) are not configured. Set COPILOT_GITHUB_TOKEN or COPILOT_PROVIDER_API_KEY to enable this provider.', + }, + unconfiguredResponseWhen: () => (oidcConfigured + ? { + kind: 'provider_not_configured', + message: `Copilot OIDC token (${authProvider}) unavailable; retry shortly`, + } + : null), + healthServiceName: 'awf-api-proxy-copilot', + missingCredentialMessage: 'COPILOT_GITHUB_TOKEN or COPILOT_PROVIDER_API_KEY not configured in api-proxy sidecar', + unavailableWhen: () => oidcConfigured ? { message: `Copilot OIDC token (${authProvider}) not yet available in api-proxy sidecar` } : null, + extra: { + // Exposed for introspection / testing + _githubToken: githubToken, + _apiKey: apiKey, + _integrationId: integrationId, + _rawTarget: rawTarget, + _basePath: basePath, + _oidcProvider: oidcProvider, + _awsOidcProvider: awsOidcProvider, + }, + }), /** * Build Copilot auth headers for this request. * * The Copilot /models endpoint only accepts COPILOT_GITHUB_TOKEN (GitHub OAuth). * All other requests use the resolved auth token (COPILOT_PROVIDER_API_KEY when real, otherwise COPILOT_GITHUB_TOKEN). * - * @param {import('http').IncomingMessage} req + * @param {{ req: import('http').IncomingMessage, resolveHeaders: () => Record }} params * @returns {Record} */ - getAuthHeaders(req) { + getAuthHeaders({ req, resolveHeaders }) { let reqPathname; try { reqPathname = new URL(req.url, 'http://localhost').pathname; @@ -234,32 +252,7 @@ function buildCopilotModelsRequest(extra = {}) { return withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId); } - return resolveInferenceHeaders(); - }, - bodyTransform, - missingCredentialResponse: { - kind: 'provider_not_configured', - message: 'Credentials for GitHub Copilot (port 10002) are not configured. Set COPILOT_GITHUB_TOKEN or COPILOT_PROVIDER_API_KEY to enable this provider.', - }, - unconfiguredResponseWhen: () => (oidcConfigured - ? { - kind: 'provider_not_configured', - message: `Copilot OIDC token (${authProvider}) unavailable; retry shortly`, - } - : null), - healthServiceName: 'awf-api-proxy-copilot', - missingCredentialMessage: 'COPILOT_GITHUB_TOKEN or COPILOT_PROVIDER_API_KEY not configured in api-proxy sidecar', - unavailableWhen: () => oidcConfigured ? { message: `Copilot OIDC token (${authProvider}) not yet available in api-proxy sidecar` } : null, - extra: { - ...oidcRuntimeMethods, - // Exposed for introspection / testing - _githubToken: githubToken, - _apiKey: apiKey, - _integrationId: integrationId, - _rawTarget: rawTarget, - _basePath: basePath, - _oidcProvider: oidcProvider, - _awsOidcProvider: awsOidcProvider, + return resolveHeaders(); }, }); } diff --git a/containers/api-proxy/providers/openai.js b/containers/api-proxy/providers/openai.js index 5510df901..422026369 100644 --- a/containers/api-proxy/providers/openai.js +++ b/containers/api-proxy/providers/openai.js @@ -17,8 +17,7 @@ const { const { validateAuthHeaderEnv } = require('../oidc-adapter-utils'); const { bearerAuthHeaders, providerKeyHeaders } = require('./auth-headers'); -const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); -const { createProviderOidcHeaderStrategy } = require('./cloud-oidc-init'); +const { createProviderAuthScaffold, createOidcAwareProviderAdapter } = require('../adapter-factory'); const { OPENAI_ENV, COPILOT_ENV } = require('../provider-env-constants'); /** @@ -71,63 +70,52 @@ function createOpenAIAdapter(env, deps = {}) { return bearerAuthHeaders(key); } const buildStaticAuthHeaders = () => buildTokenAuthHeaders(apiKey); - const { - authProvider, oidcConfigured, - runtimeMethods: oidcRuntimeMethods, - validationSkip, - skipModelsFetch, - resolveHeaders: resolveOidcHeaders, - } = createProviderOidcHeaderStrategy(env, { staticAuthToken: apiKey }, { + return createOidcAwareProviderAdapter({ + env, + oidcAuthOptions: { staticAuthToken: apiKey }, buildOidcHeaders: buildTokenAuthHeaders, buildStaticHeaders: buildStaticAuthHeaders, - }); - - const adapterMethods = createAdapterMethods({ - apiKey, - rawTarget, - basePath, - provider: 'openai', - port: 10000, - defaultTarget: 'api.openai.com', - validationPath: '/v1/models', - validationHeaders: buildStaticAuthHeaders, - validationSkip, - skipModelsFetch, - modelsPath: '/v1/models', - modelsFetchHeaders: buildStaticAuthHeaders, - reflectionConfigured: !!apiKey || oidcConfigured, - reflectionModelsPath: '/v1/models', - reflectionExtra: () => ({ - auth_type: oidcConfigured ? `github-oidc/${authProvider}` : 'static-key', + createAdapterMethodsOptions: ({ authProvider, oidcConfigured, validationSkip, skipModelsFetch }) => ({ + apiKey, + rawTarget, + basePath, + provider: 'openai', + port: 10000, + defaultTarget: 'api.openai.com', + validationPath: '/v1/models', + validationHeaders: buildStaticAuthHeaders, + validationSkip, + skipModelsFetch, + modelsPath: '/v1/models', + modelsFetchHeaders: buildStaticAuthHeaders, + reflectionConfigured: !!apiKey || oidcConfigured, + reflectionModelsPath: '/v1/models', + reflectionExtra: () => ({ + auth_type: oidcConfigured ? `github-oidc/${authProvider}` : 'static-key', + }), + }), + buildAdapterOptions: ({ oidcConfigured }) => ({ + name: 'openai', + port: 10000, + isManagementPort: true, + bodyTransform, + missingCredentialResponse: { + kind: 'plain_error', + statusCode: 404, + message: 'OpenAI proxy not configured (no OPENAI_API_KEY/COPILOT_PROVIDER_API_KEY or OIDC auth)', + }, + unconfiguredResponseWhen: () => (oidcConfigured + ? { + kind: 'plain_error', + statusCode: 503, + message: 'OpenAI OIDC token unavailable; retry shortly', + } + : null), + extra: { + /** Port 10000 always counts toward the startup validation latch. */ + participatesInValidation: true, + }, }), - }); - - return buildProviderAdapter({ - name: 'openai', - port: 10000, - isManagementPort: true, - adapterMethods, - getAuthHeaders() { - return resolveOidcHeaders(); - }, - bodyTransform, - missingCredentialResponse: { - kind: 'plain_error', - statusCode: 404, - message: 'OpenAI proxy not configured (no OPENAI_API_KEY/COPILOT_PROVIDER_API_KEY or OIDC auth)', - }, - unconfiguredResponseWhen: () => (oidcConfigured - ? { - kind: 'plain_error', - statusCode: 503, - message: 'OpenAI OIDC token unavailable; retry shortly', - } - : null), - extra: { - ...oidcRuntimeMethods, - /** Port 10000 always counts toward the startup validation latch. */ - participatesInValidation: true, - }, }); }