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
66 changes: 65 additions & 1 deletion containers/api-proxy/adapter-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
*
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
65 changes: 65 additions & 0 deletions containers/api-proxy/adapter-factory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
24 changes: 10 additions & 14 deletions containers/api-proxy/providers/anthropic.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

const {
composeBodyTransforms,
makeProviderNotConfiguredResponse,
} = require('../proxy-utils');
const {
validateAuthHeaderEnv,
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 9 additions & 15 deletions containers/api-proxy/providers/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

const {
normalizeBasePath,
makeProviderNotConfiguredResponse,
composeBodyTransforms,
} = require('../proxy-utils');
const { resolveOidcAuthHeaders } = require('../oidc-adapter-utils');
Expand Down Expand Up @@ -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,
Expand Down
23 changes: 11 additions & 12 deletions containers/api-proxy/providers/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading