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
3 changes: 2 additions & 1 deletion containers/api-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ COPY server.js logging.js metrics.js rate-limiter.js \
ai-credits-pricing.js models-dev-catalog.js models.dev.catalog.json \
oidc-refresh-utils.js body-transform.js body-utils.js rate-limit.js websocket-proxy.js \
deprecated-header-tracker.js billing-headers.js upstream-response.js \
anthropic-cache.js otel.js token-budget-log.js blocked-request-diagnostics.js ./
anthropic-cache.js otel.js token-budget-log.js blocked-request-diagnostics.js \
provider-env-constants.js ./
COPY guards/ ./guards/
COPY providers/ ./providers/
COPY transforms/ ./transforms/
Expand Down
50 changes: 50 additions & 0 deletions containers/api-proxy/provider-env-constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

/**
* Environment variable name constants for the API proxy provider adapters.
*
* This is the single source of truth for env var names on the container JS side.
* The TypeScript equivalent lives in src/api-proxy-env-constants.ts.
*
* Both files must be kept in sync when adding or renaming env vars.
*/

/** Environment variable names for the OpenAI provider adapter. */
const OPENAI_ENV = /** @type {const} */ ({
KEY: 'OPENAI_API_KEY',
TARGET: 'OPENAI_API_TARGET',
BASE_PATH: 'OPENAI_API_BASE_PATH',
AUTH_HEADER: 'AWF_OPENAI_AUTH_HEADER',
});

/** Environment variable names for the Anthropic provider adapter. */
const ANTHROPIC_ENV = /** @type {const} */ ({
KEY: 'ANTHROPIC_API_KEY',
TARGET: 'ANTHROPIC_API_TARGET',
BASE_PATH: 'ANTHROPIC_API_BASE_PATH',
AUTH_HEADER: 'AWF_ANTHROPIC_AUTH_HEADER',
});

/** Environment variable names for the Gemini provider adapter. */
const GEMINI_ENV = /** @type {const} */ ({
KEY: 'GEMINI_API_KEY',
TARGET: 'GEMINI_API_TARGET',
BASE_PATH: 'GEMINI_API_BASE_PATH',
});

/** Environment variable names for the Copilot provider adapter. */
const COPILOT_ENV = /** @type {const} */ ({
GITHUB_TOKEN: 'COPILOT_GITHUB_TOKEN',
PROVIDER_API_KEY: 'COPILOT_PROVIDER_API_KEY',
PROVIDER_TYPE: 'COPILOT_PROVIDER_TYPE',
PROVIDER_BASE_URL: 'COPILOT_PROVIDER_BASE_URL',
API_TARGET: 'COPILOT_API_TARGET',
API_BASE_PATH: 'COPILOT_API_BASE_PATH',
});

module.exports = {
OPENAI_ENV,
ANTHROPIC_ENV,
GEMINI_ENV,
COPILOT_ENV,
};
9 changes: 5 additions & 4 deletions containers/api-proxy/providers/anthropic.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
} = require('../proxy-utils');
const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { AnthropicOidcTokenProvider } = require('../anthropic-oidc-token-provider');
const { ANTHROPIC_ENV } = require('../provider-env-constants');
const { createProviderOidcAuth } = require('./cloud-oidc-init');

let makeAnthropicTransform, loadCustomTransform, EXTENDED_CACHE_BETA;
Expand All @@ -45,12 +46,12 @@ try {
*/
function createAnthropicAdapter(env, deps = {}) {
const { apiKey, rawTarget, basePath } = createBaseAdapterConfig(env, {
keyEnvVar: 'ANTHROPIC_API_KEY',
targetEnvVar: 'ANTHROPIC_API_TARGET',
basePathEnvVar: 'ANTHROPIC_API_BASE_PATH',
keyEnvVar: ANTHROPIC_ENV.KEY,
targetEnvVar: ANTHROPIC_ENV.TARGET,
basePathEnvVar: ANTHROPIC_ENV.BASE_PATH,
defaultTarget: 'api.anthropic.com',
});
const authHeaderName = validateAuthHeaderEnv('AWF_ANTHROPIC_AUTH_HEADER', env.AWF_ANTHROPIC_AUTH_HEADER, 'x-api-key');
const authHeaderName = validateAuthHeaderEnv(ANTHROPIC_ENV.AUTH_HEADER, env[ANTHROPIC_ENV.AUTH_HEADER], 'x-api-key');

// oidcRequested tracks whether the caller asked for Anthropic OIDC, regardless
// of whether the token env vars (ACTIONS_ID_TOKEN_REQUEST_*) are also present.
Expand Down
5 changes: 3 additions & 2 deletions containers/api-proxy/providers/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const {
} = require('./copilot-auth');
const { createProviderOidcAuth } = require('./cloud-oidc-init');
const { URL } = require('url');
const { COPILOT_ENV } = require('../provider-env-constants');

/**
* Create the GitHub Copilot provider adapter.
Expand All @@ -49,13 +50,13 @@ const { URL } = require('url');
* @returns {import('./index').ProviderAdapter}
*/
function createCopilotAdapter(env, deps = {}) {
const githubToken = stripBearerPrefix(env.COPILOT_GITHUB_TOKEN);
const githubToken = stripBearerPrefix(env[COPILOT_ENV.GITHUB_TOKEN]);
// resolveApiKey filters out the AWF placeholder so it is never used as a real BYOK credential.
const apiKey = resolveApiKey(env);
const staticAuthToken = resolveCopilotAuthToken(env);
const integrationId = env.COPILOT_INTEGRATION_ID || 'agentic-workflows';
const rawTarget = deriveCopilotApiTarget(env);
const basePath = normalizeBasePath(env.COPILOT_API_BASE_PATH);
const basePath = normalizeBasePath(env[COPILOT_ENV.API_BASE_PATH]);

// OIDC auth strategy (Azure OpenAI via Entra, AWS Bedrock, GCP Vertex AI) for
// BYOK targets pointed at by COPILOT_PROVIDER_BASE_URL. Mirrors the OpenAI
Expand Down
7 changes: 4 additions & 3 deletions containers/api-proxy/providers/gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

const { stripGeminiKeyParam, makeUnconfiguredHealthResponse } = require('../proxy-utils');
const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { GEMINI_ENV } = require('../provider-env-constants');

/**
* Create the Google Gemini provider adapter.
Expand All @@ -25,9 +26,9 @@ const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } =
*/
function createGeminiAdapter(env, deps = {}) {
const { apiKey, rawTarget, basePath } = createBaseAdapterConfig(env, {
keyEnvVar: 'GEMINI_API_KEY',
targetEnvVar: 'GEMINI_API_TARGET',
basePathEnvVar: 'GEMINI_API_BASE_PATH',
keyEnvVar: GEMINI_ENV.KEY,
targetEnvVar: GEMINI_ENV.TARGET,
basePathEnvVar: GEMINI_ENV.BASE_PATH,
defaultTarget: 'generativelanguage.googleapis.com',
});

Expand Down
17 changes: 9 additions & 8 deletions containers/api-proxy/providers/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {

const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { createProviderOidcAuth } = require('./cloud-oidc-init');
const { OPENAI_ENV, COPILOT_ENV } = require('../provider-env-constants');

/**
* Create the OpenAI provider adapter.
Expand All @@ -28,26 +29,26 @@ const { createProviderOidcAuth } = require('./cloud-oidc-init');
*/
function createOpenAIAdapter(env, deps = {}) {
const { apiKey: openaiApiKey, rawTarget: openaiTarget, basePath: openaiBasePath } = createBaseAdapterConfig(env, {
keyEnvVar: 'OPENAI_API_KEY',
targetEnvVar: 'OPENAI_API_TARGET',
basePathEnvVar: 'OPENAI_API_BASE_PATH',
keyEnvVar: OPENAI_ENV.KEY,
targetEnvVar: OPENAI_ENV.TARGET,
basePathEnvVar: OPENAI_ENV.BASE_PATH,
defaultTarget: 'api.openai.com',
});
const providerType = (env.COPILOT_PROVIDER_TYPE || '').trim().toLowerCase();
const providerType = (env[COPILOT_ENV.PROVIDER_TYPE] || '').trim().toLowerCase();
const copilotAzureByokEnabled = providerType === 'azure';
const customAuthHeader = (() => {
const header = validateAuthHeaderEnv('AWF_OPENAI_AUTH_HEADER', env.AWF_OPENAI_AUTH_HEADER);
const header = validateAuthHeaderEnv(OPENAI_ENV.AUTH_HEADER, env[OPENAI_ENV.AUTH_HEADER]);
if (header) return header;
// Azure OpenAI BYOK uses `api-key` header instead of `Authorization: Bearer`
// (but OIDC auth still requires `Authorization: Bearer` unless explicitly overridden)
if (copilotAzureByokEnabled && (env.AWF_AUTH_TYPE || '').trim().toLowerCase() !== 'github-oidc') return 'api-key';
return '';
})();
const copilotByokApiKey = (env.COPILOT_PROVIDER_API_KEY || '').trim() || undefined;
const { target: copilotByokTarget, basePath: copilotByokBasePath } = parseApiTargetAndBasePath(env.COPILOT_PROVIDER_BASE_URL);
const copilotByokApiKey = (env[COPILOT_ENV.PROVIDER_API_KEY] || '').trim() || undefined;
const { target: copilotByokTarget, basePath: copilotByokBasePath } = parseApiTargetAndBasePath(env[COPILOT_ENV.PROVIDER_BASE_URL]);

const apiKey = openaiApiKey || (copilotAzureByokEnabled ? copilotByokApiKey : undefined);
const explicitOpenAITarget = env.OPENAI_API_TARGET ? openaiTarget : undefined;
const explicitOpenAITarget = env[OPENAI_ENV.TARGET] ? openaiTarget : undefined;
const rawTarget = explicitOpenAITarget || (copilotAzureByokEnabled ? copilotByokTarget : undefined) || 'api.openai.com';
const explicitBasePath = openaiBasePath || (copilotAzureByokEnabled ? copilotByokBasePath : '');

Expand Down
41 changes: 41 additions & 0 deletions src/api-proxy-env-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Environment variable name constants for the API proxy provider adapters.
*
* This is the single source of truth for env var names on the TypeScript host side.
* The CommonJS equivalent lives in containers/api-proxy/provider-env-constants.js.
*
* Both files must be kept in sync when adding or renaming env vars.
*/

/** Environment variable names for the OpenAI provider adapter. */
export const OPENAI_ENV = {
KEY: 'OPENAI_API_KEY',
TARGET: 'OPENAI_API_TARGET',
BASE_PATH: 'OPENAI_API_BASE_PATH',
AUTH_HEADER: 'AWF_OPENAI_AUTH_HEADER',
} as const;

/** Environment variable names for the Anthropic provider adapter. */
export const ANTHROPIC_ENV = {
KEY: 'ANTHROPIC_API_KEY',
TARGET: 'ANTHROPIC_API_TARGET',
BASE_PATH: 'ANTHROPIC_API_BASE_PATH',
AUTH_HEADER: 'AWF_ANTHROPIC_AUTH_HEADER',
} as const;

/** Environment variable names for the Gemini provider adapter. */
export const GEMINI_ENV = {
KEY: 'GEMINI_API_KEY',
TARGET: 'GEMINI_API_TARGET',
BASE_PATH: 'GEMINI_API_BASE_PATH',
} as const;

/** Environment variable names for the Copilot provider adapter. */
export const COPILOT_ENV = {
GITHUB_TOKEN: 'COPILOT_GITHUB_TOKEN',
PROVIDER_API_KEY: 'COPILOT_PROVIDER_API_KEY',
PROVIDER_TYPE: 'COPILOT_PROVIDER_TYPE',
PROVIDER_BASE_URL: 'COPILOT_PROVIDER_BASE_URL',
API_TARGET: 'COPILOT_API_TARGET',
API_BASE_PATH: 'COPILOT_API_BASE_PATH',
} as const;
31 changes: 16 additions & 15 deletions src/commands/build-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WrapperConfig, LogLevel, UpstreamProxyConfig } from '../types';
import { OPENAI_ENV, ANTHROPIC_ENV, GEMINI_ENV, COPILOT_ENV } from '../api-proxy-env-constants';

/**
* Inputs required to assemble a {@link WrapperConfig}.
Expand Down Expand Up @@ -170,38 +171,38 @@ export function buildConfig(inputs: BuildConfigInputs): WrapperConfig {
: undefined),
maxCapturedBytes: (options.maxCapturedBytes as number | undefined) ??
(process.env.AWF_MAX_BLOCKED_CAPTURE_BYTES ? Number(process.env.AWF_MAX_BLOCKED_CAPTURE_BYTES) : undefined),
openaiApiKey: process.env.OPENAI_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
copilotGithubToken: process.env.COPILOT_GITHUB_TOKEN,
copilotProviderApiKey: process.env.COPILOT_PROVIDER_API_KEY,
openaiApiKey: process.env[OPENAI_ENV.KEY],
anthropicApiKey: process.env[ANTHROPIC_ENV.KEY],
copilotGithubToken: process.env[COPILOT_ENV.GITHUB_TOKEN],
copilotProviderApiKey: process.env[COPILOT_ENV.PROVIDER_API_KEY],
copilotProviderType:
(options.copilotProviderType as string | undefined) || process.env.COPILOT_PROVIDER_TYPE,
(options.copilotProviderType as string | undefined) || process.env[COPILOT_ENV.PROVIDER_TYPE],
copilotProviderBaseUrl:
(options.copilotProviderBaseUrl as string | undefined) || process.env.COPILOT_PROVIDER_BASE_URL,
geminiApiKey: process.env.GEMINI_API_KEY,
(options.copilotProviderBaseUrl as string | undefined) || process.env[COPILOT_ENV.PROVIDER_BASE_URL],
geminiApiKey: process.env[GEMINI_ENV.KEY],
copilotApiTarget: resolvedCopilotApiTarget,
copilotApiBasePath: resolvedCopilotApiBasePath,
copilotByokExtraHeaders: options.copilotByokExtraHeaders as Record<string, string> | undefined,
copilotByokExtraBodyFields: options.copilotByokExtraBodyFields as Record<string, string> | undefined,
copilotByokSessionId: options.copilotByokSessionId as string | undefined,
openaiApiTarget:
(options.openaiApiTarget as string | undefined) || process.env.OPENAI_API_TARGET,
(options.openaiApiTarget as string | undefined) || process.env[OPENAI_ENV.TARGET],
openaiApiBasePath:
(options.openaiApiBasePath as string | undefined) || process.env.OPENAI_API_BASE_PATH,
(options.openaiApiBasePath as string | undefined) || process.env[OPENAI_ENV.BASE_PATH],
anthropicApiTarget:
(options.anthropicApiTarget as string | undefined) || process.env.ANTHROPIC_API_TARGET,
(options.anthropicApiTarget as string | undefined) || process.env[ANTHROPIC_ENV.TARGET],
anthropicApiBasePath:
(options.anthropicApiBasePath as string | undefined) || process.env.ANTHROPIC_API_BASE_PATH,
(options.anthropicApiBasePath as string | undefined) || process.env[ANTHROPIC_ENV.BASE_PATH],
openaiApiAuthHeader:
(options.openaiApiAuthHeader as string | undefined) || process.env.AWF_OPENAI_AUTH_HEADER,
(options.openaiApiAuthHeader as string | undefined) || process.env[OPENAI_ENV.AUTH_HEADER],
anthropicApiAuthHeader:
(options.anthropicApiAuthHeader as string | undefined) || process.env.AWF_ANTHROPIC_AUTH_HEADER,
(options.anthropicApiAuthHeader as string | undefined) || process.env[ANTHROPIC_ENV.AUTH_HEADER],
anthropicTokenUrl:
(options.anthropicTokenUrl as string | undefined) || process.env.AWF_AUTH_ANTHROPIC_TOKEN_URL,
geminiApiTarget:
(options.geminiApiTarget as string | undefined) || process.env.GEMINI_API_TARGET,
(options.geminiApiTarget as string | undefined) || process.env[GEMINI_ENV.TARGET],
geminiApiBasePath:
(options.geminiApiBasePath as string | undefined) || process.env.GEMINI_API_BASE_PATH,
(options.geminiApiBasePath as string | undefined) || process.env[GEMINI_ENV.BASE_PATH],
difcProxyHost: options.difcProxyHost as string | undefined,
difcProxyCaCert: options.difcProxyCaCert as string | undefined,
githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN,
Expand Down
31 changes: 16 additions & 15 deletions src/services/api-proxy-service-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getConfigEnvValue, getLowerCaseProcessEnvValue, pickEnvVars } from '../
import { NetworkConfig, ImageBuildConfig } from './squid-service';
import { applyHostPathPrefixToVolumes } from './host-path-prefix';
import { buildContainerSecurityHardening } from './service-security';
import { OPENAI_ENV, ANTHROPIC_ENV, GEMINI_ENV, COPILOT_ENV } from '../api-proxy-env-constants';

interface ApiProxyServiceConfigParams {
config: WrapperConfig;
Expand All @@ -22,17 +23,17 @@ interface ApiProxyServiceConfigParams {
* Centralizes the repetitive per-provider target/basePath conditional env generation.
*/
function buildProviderTargetEnv(config: WrapperConfig): Record<string, string> {
const copilotProviderType = config.copilotProviderType || getConfigEnvValue(config, 'COPILOT_PROVIDER_TYPE');
const copilotProviderBaseUrl = config.copilotProviderBaseUrl || getConfigEnvValue(config, 'COPILOT_PROVIDER_BASE_URL');
const copilotProviderType = config.copilotProviderType || getConfigEnvValue(config, COPILOT_ENV.PROVIDER_TYPE);
const copilotProviderBaseUrl = config.copilotProviderBaseUrl || getConfigEnvValue(config, COPILOT_ENV.PROVIDER_BASE_URL);
const copilotProviderApiKey = config.copilotProviderApiKey;

const env: Record<string, string> = {};

const providers: Array<{ target?: string; basePath?: string; envTarget: string; envBasePath: string; stripTarget?: boolean }> = [
{ target: config.copilotApiTarget, basePath: config.copilotApiBasePath, envTarget: 'COPILOT_API_TARGET', envBasePath: 'COPILOT_API_BASE_PATH', stripTarget: true },
{ target: config.openaiApiTarget, basePath: config.openaiApiBasePath, envTarget: 'OPENAI_API_TARGET', envBasePath: 'OPENAI_API_BASE_PATH', stripTarget: true },
{ target: config.anthropicApiTarget, basePath: config.anthropicApiBasePath, envTarget: 'ANTHROPIC_API_TARGET', envBasePath: 'ANTHROPIC_API_BASE_PATH', stripTarget: true },
{ target: config.geminiApiTarget, basePath: config.geminiApiBasePath, envTarget: 'GEMINI_API_TARGET', envBasePath: 'GEMINI_API_BASE_PATH', stripTarget: true },
{ target: config.copilotApiTarget, basePath: config.copilotApiBasePath, envTarget: COPILOT_ENV.API_TARGET, envBasePath: COPILOT_ENV.API_BASE_PATH, stripTarget: true },
{ target: config.openaiApiTarget, basePath: config.openaiApiBasePath, envTarget: OPENAI_ENV.TARGET, envBasePath: OPENAI_ENV.BASE_PATH, stripTarget: true },
{ target: config.anthropicApiTarget, basePath: config.anthropicApiBasePath, envTarget: ANTHROPIC_ENV.TARGET, envBasePath: ANTHROPIC_ENV.BASE_PATH, stripTarget: true },
{ target: config.geminiApiTarget, basePath: config.geminiApiBasePath, envTarget: GEMINI_ENV.TARGET, envBasePath: GEMINI_ENV.BASE_PATH, stripTarget: true },
];

for (const { target, basePath, envTarget, envBasePath, stripTarget } of providers) {
Expand All @@ -41,9 +42,9 @@ function buildProviderTargetEnv(config: WrapperConfig): Record<string, string> {
}

// Copilot-specific provider passthrough
if (copilotProviderType) env.COPILOT_PROVIDER_TYPE = copilotProviderType;
if (copilotProviderBaseUrl) env.COPILOT_PROVIDER_BASE_URL = copilotProviderBaseUrl;
if (copilotProviderApiKey) env.COPILOT_PROVIDER_API_KEY = copilotProviderApiKey;
if (copilotProviderType) env[COPILOT_ENV.PROVIDER_TYPE] = copilotProviderType;
if (copilotProviderBaseUrl) env[COPILOT_ENV.PROVIDER_BASE_URL] = copilotProviderBaseUrl;
if (copilotProviderApiKey) env[COPILOT_ENV.PROVIDER_API_KEY] = copilotProviderApiKey;

// Pre-startup model validation (non-sensitive config value).
// Prefer explicit requestedModel, but fall back to COPILOT_MODEL when present so
Expand Down Expand Up @@ -101,10 +102,10 @@ export function buildApiProxyServiceConfig(params: ApiProxyServiceConfigParams):
),
environment: {
// Pass API keys securely to sidecar (not visible to agent)
...(config.openaiApiKey && { OPENAI_API_KEY: config.openaiApiKey }),
...(config.anthropicApiKey && { ANTHROPIC_API_KEY: config.anthropicApiKey }),
...(config.copilotGithubToken && { COPILOT_GITHUB_TOKEN: config.copilotGithubToken }),
...(config.geminiApiKey && { GEMINI_API_KEY: config.geminiApiKey }),
...(config.openaiApiKey && { [OPENAI_ENV.KEY]: config.openaiApiKey }),
...(config.anthropicApiKey && { [ANTHROPIC_ENV.KEY]: config.anthropicApiKey }),
...(config.copilotGithubToken && { [COPILOT_ENV.GITHUB_TOKEN]: config.copilotGithubToken }),
...(config.geminiApiKey && { [GEMINI_ENV.KEY]: config.geminiApiKey }),
// Configurable API targets (for GHES/GHEC / custom endpoints)
// Strip any scheme prefix — server.js also normalizes defensively, but
// stripping here prevents a scheme-prefixed hostname from reaching the
Expand Down Expand Up @@ -259,8 +260,8 @@ export function buildApiProxyServiceConfig(params: ApiProxyServiceConfigParams):
'AWF_ANTHROPIC_STRIP_ANSI',
),
// Custom auth header names for internal AI gateways
...(config.openaiApiAuthHeader && { AWF_OPENAI_AUTH_HEADER: config.openaiApiAuthHeader }),
...(config.anthropicApiAuthHeader && { AWF_ANTHROPIC_AUTH_HEADER: config.anthropicApiAuthHeader }),
...(config.openaiApiAuthHeader && { [OPENAI_ENV.AUTH_HEADER]: config.openaiApiAuthHeader }),
...(config.anthropicApiAuthHeader && { [ANTHROPIC_ENV.AUTH_HEADER]: config.anthropicApiAuthHeader }),
...(config.anthropicTokenUrl && { AWF_AUTH_ANTHROPIC_TOKEN_URL: config.anthropicTokenUrl }),
// NOTE: AWF_ANTHROPIC_TRANSFORM_FILE is intentionally NOT forwarded from the host.
// The api-proxy container holds live API credentials; loading arbitrary host-side JS
Expand Down
Loading