From 45efe4c50d1c581c5824e7e918b5877499918bcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:53:35 +0000 Subject: [PATCH 1/2] Initial plan From d55d0fef952e7a921dec2d4d0451e11b319ea8b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:00:17 +0000 Subject: [PATCH 2/2] refactor: consolidate provider unconfigured responses --- containers/api-proxy/adapter-factory.js | 66 +++++++++++++++++++- containers/api-proxy/adapter-factory.test.js | 65 +++++++++++++++++++ containers/api-proxy/providers/anthropic.js | 24 +++---- containers/api-proxy/providers/copilot.js | 24 +++---- containers/api-proxy/providers/openai.js | 23 ++++--- 5 files changed, 160 insertions(+), 42 deletions(-) diff --git a/containers/api-proxy/adapter-factory.js b/containers/api-proxy/adapter-factory.js index 3d53db835..3cd7a1f65 100644 --- a/containers/api-proxy/adapter-factory.js +++ b/containers/api-proxy/adapter-factory.js @@ -12,7 +12,37 @@ 'use strict'; -const { normalizeApiTarget, normalizeBasePath, makeUnconfiguredHealthResponse } = require('./proxy-utils'); +const { + normalizeApiTarget, + normalizeBasePath, + makeProviderNotConfiguredResponse, + makeUnconfiguredHealthResponse, +} = require('./proxy-utils'); + +/** + * @param {string} provider + * @param {number} port + * @param {{ + * kind: 'plain_error', + * message: string, + * statusCode: number + * }|{ + * kind: 'provider_not_configured', + * message: string, + * statusCode?: number + * }} spec + * @returns {import('./providers/index').UnconfiguredResponse} + */ +function buildUnconfiguredResponse(provider, port, spec) { + if (spec.kind === 'plain_error') { + return { statusCode: spec.statusCode, body: { error: spec.message } }; + } + const response = makeProviderNotConfiguredResponse(provider, port, spec.message); + if (spec.statusCode !== undefined) { + response.statusCode = spec.statusCode; + } + return response; +} /** * @@ -204,6 +234,24 @@ function createProviderAuthScaffold(env, deps = {}, { keyEnvVar, targetEnvVar, b * @param {(() => boolean)} [opts.isEnabled] - Optional isEnabled override (must be provided either here or via `extra`) * @param {((url: string) => string)} [opts.transformRequestUrl] - Optional URL transformer * @param {(() => import('./providers/index').UnconfiguredResponse)} [opts.getUnconfiguredResponse] - Optional not-configured response + * @param {{ + * kind: 'plain_error', + * message: string, + * statusCode: number + * }|{ + * kind: 'provider_not_configured', + * message: string, + * statusCode?: number + * }} [opts.missingCredentialResponse] - Declarative default request-time not-configured response + * @param {(() => ({ + * kind: 'plain_error', + * message: string, + * statusCode: number + * }|{ + * kind: 'provider_not_configured', + * message: string, + * statusCode?: number + * }|null))} [opts.unconfiguredResponseWhen] - Optional override callback for request-time not-configured response * @param {(() => import('./providers/index').UnconfiguredResponse)} [opts.getUnconfiguredHealthResponse] - Optional explicit not-configured /health response (takes precedence over declarative metadata) * @param {string} [opts.healthServiceName] - Service name for auto-generated /health response (e.g. 'awf-api-proxy-gemini'); requires missingCredentialMessage * @param {string} [opts.missingCredentialMessage] - Default error message when credentials are absent; requires healthServiceName @@ -222,12 +270,28 @@ function buildProviderAdapter({ isEnabled, transformRequestUrl, getUnconfiguredResponse, + missingCredentialResponse, + unconfiguredResponseWhen, getUnconfiguredHealthResponse, healthServiceName, missingCredentialMessage, unavailableWhen, extra = {}, }) { + const hasDeclarativeRequestMetadata = + missingCredentialResponse !== undefined || unconfiguredResponseWhen !== undefined; + if (getUnconfiguredResponse === undefined && hasDeclarativeRequestMetadata) { + if (missingCredentialResponse === undefined) { + throw new TypeError( + `Provider adapter "${name}" declarative request metadata requires missingCredentialResponse`, + ); + } + getUnconfiguredResponse = () => { + const override = unconfiguredResponseWhen ? unconfiguredResponseWhen() : null; + return buildUnconfiguredResponse(name, port, override || missingCredentialResponse); + }; + } + // Auto-generate getUnconfiguredHealthResponse from declarative metadata when // no explicit function is provided. The optional unavailableWhen callback // allows providers with OIDC to surface a dynamic message/status. diff --git a/containers/api-proxy/adapter-factory.test.js b/containers/api-proxy/adapter-factory.test.js index 566d09da4..6670d6558 100644 --- a/containers/api-proxy/adapter-factory.test.js +++ b/containers/api-proxy/adapter-factory.test.js @@ -185,6 +185,71 @@ describe('buildProviderAdapter', () => { expect(adapter.getUnconfiguredResponse).toBe(getUnconfiguredResponse); }); + it('auto-generates getUnconfiguredResponse from missingCredentialResponse metadata', () => { + const adapter = buildProviderAdapter({ + name: 'test', + port: 10099, + adapterMethods: makeAdapterMethods(), + getAuthHeaders() { return {}; }, + isEnabled() { return false; }, + missingCredentialResponse: { + kind: 'provider_not_configured', + message: 'TEST_API_KEY not configured', + }, + }); + + expect(adapter.getUnconfiguredResponse()).toEqual({ + statusCode: 503, + body: { + error: { + message: 'TEST_API_KEY not configured', + type: 'provider_not_configured', + provider: 'test', + port: 10099, + }, + }, + }); + }); + + it('auto-generated getUnconfiguredResponse uses unconfiguredResponseWhen override when present', () => { + const adapter = buildProviderAdapter({ + name: 'test', + port: 10099, + adapterMethods: makeAdapterMethods(), + getAuthHeaders() { return {}; }, + isEnabled() { return false; }, + missingCredentialResponse: { + kind: 'provider_not_configured', + message: 'TEST_API_KEY not configured', + }, + unconfiguredResponseWhen: () => ({ + kind: 'plain_error', + statusCode: 503, + message: 'OIDC token unavailable; retry shortly', + }), + }); + + expect(adapter.getUnconfiguredResponse()).toEqual({ + statusCode: 503, + body: { error: 'OIDC token unavailable; retry shortly' }, + }); + }); + + it('throws when declarative request metadata is partially specified', () => { + expect(() => buildProviderAdapter({ + name: 'test', + port: 10099, + adapterMethods: makeAdapterMethods(), + getAuthHeaders() { return {}; }, + isEnabled() { return false; }, + unconfiguredResponseWhen: () => ({ + kind: 'plain_error', + statusCode: 503, + message: 'OIDC token unavailable; retry shortly', + }), + })).toThrow('declarative request metadata requires missingCredentialResponse'); + }); + it('omits getUnconfiguredHealthResponse when not provided', () => { const adapter = buildProviderAdapter({ name: 'test', diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 6edcdffff..e82cc8a78 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -14,7 +14,6 @@ const { composeBodyTransforms, - makeProviderNotConfiguredResponse, } = require('../proxy-utils'); const { validateAuthHeaderEnv, @@ -207,20 +206,17 @@ function createAnthropicAdapter(env, deps = {}) { return headers; }, bodyTransform: composedBodyTransform, - /** Response returned for all requests when no ANTHROPIC_API_KEY is configured. */ - getUnconfiguredResponse() { - if (oidcRequested) { - return { - statusCode: 503, - body: { error: oidcUnavailableError }, - }; - } - return makeProviderNotConfiguredResponse( - 'anthropic', - 10001, - 'Credentials for Anthropic (port 10001) are not configured. Set ANTHROPIC_API_KEY to enable this provider.' - ); + 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, diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index 8b74e7cb9..e3de14713 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -19,7 +19,6 @@ const { normalizeBasePath, - makeProviderNotConfiguredResponse, composeBodyTransforms, } = require('../proxy-utils'); const { resolveOidcAuthHeaders } = require('../oidc-adapter-utils'); @@ -246,21 +245,16 @@ function buildCopilotModelsRequest(extra = {}) { }, integrationId); }, bodyTransform, - /** Response returned for all requests when no Copilot credentials are configured. */ - getUnconfiguredResponse() { - if (oidcConfigured) { - return makeProviderNotConfiguredResponse( - 'copilot', - 10002, - `Copilot OIDC token (${authProvider}) unavailable; retry shortly` - ); - } - return makeProviderNotConfiguredResponse( - 'copilot', - 10002, - 'Credentials for GitHub Copilot (port 10002) are not configured. Set COPILOT_GITHUB_TOKEN or COPILOT_PROVIDER_API_KEY to enable this provider.' - ); + 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, diff --git a/containers/api-proxy/providers/openai.js b/containers/api-proxy/providers/openai.js index 34bcebf88..def3f9e94 100644 --- a/containers/api-proxy/providers/openai.js +++ b/containers/api-proxy/providers/openai.js @@ -111,19 +111,18 @@ function createOpenAIAdapter(env, deps = {}) { ); }, bodyTransform, - /** Response returned when port 10000 receives a proxy request but no key is set. */ - getUnconfiguredResponse() { - if (oidcConfigured) { - return { - statusCode: 503, - body: { error: 'OpenAI OIDC token unavailable; retry shortly' }, - }; - } - return { - statusCode: 404, - body: { error: 'OpenAI proxy not configured (no OPENAI_API_KEY/COPILOT_PROVIDER_API_KEY or OIDC auth)' }, - }; + 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. */