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
29 changes: 4 additions & 25 deletions containers/api-proxy/providers/gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
*
* URL transform: strips ?key=, ?apiKey=, ?api_key= query params that some
* Gemini SDK versions append alongside the header.
*
* All configuration lives in GOOGLE_PROVIDER_SPECS.gemini (google-provider-specs.js).
*/

const { stripGeminiKeyParam } = require('../proxy-utils');
const { GEMINI_ENV } = require('../provider-env-constants');
const { createGoogleApiKeyAdapter } = require('./google-adapter');
const { createGoogleProviderAdapter } = require('./google-adapter');

/**
* Create the Google Gemini provider adapter.
Expand All @@ -25,28 +25,7 @@ const { createGoogleApiKeyAdapter } = require('./google-adapter');
* @returns {import('./index').ProviderAdapter}
*/
function createGeminiAdapter(env, deps = {}) {
return createGoogleApiKeyAdapter(env, deps, {
name: 'gemini',
port: 10003,
envConstants: GEMINI_ENV,
defaultTarget: 'generativelanguage.googleapis.com',
validationPath: '/v1beta/models',
modelsPath: '/v1beta/models',
healthServiceName: 'awf-api-proxy-gemini',
unconfiguredErrorMessage: 'Gemini proxy not configured (no GEMINI_API_KEY). Set GEMINI_API_KEY in the AWF runner environment to enable credential isolation.',
healthErrorMessage: 'GEMINI_API_KEY not configured in api-proxy sidecar',
/**
* Strip Gemini SDK auth query parameters before forwarding.
* The SDK injects ?key= (or ?apiKey=, ?api_key=) alongside the header;
* forwarding both causes API_KEY_INVALID errors on the upstream.
*
* @param {string} url
* @returns {string}
*/
transformRequestUrl(url) {
return stripGeminiKeyParam(url);
},
});
return createGoogleProviderAdapter('gemini', env, deps);
}

module.exports = { createGeminiAdapter };
36 changes: 35 additions & 1 deletion containers/api-proxy/providers/google-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
const { makeUnconfiguredHealthResponse } = require('../proxy-utils');
const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { providerKeyHeaders } = require('./auth-headers');
const { GOOGLE_PROVIDER_SPECS } = require('./google-provider-specs');

/**
* Create a Google API-key–based provider adapter.
Expand Down Expand Up @@ -92,4 +93,37 @@ function createGoogleApiKeyAdapter(env, deps = {}, opts) {
});
}

module.exports = { createGoogleApiKeyAdapter };
/**
* Create a Google provider adapter from its declarative spec.
*
* Error/health messaging is derived from the spec so each Google-family
* provider only needs a config entry in GOOGLE_PROVIDER_SPECS.
*
* @param {string} providerKey - Key into GOOGLE_PROVIDER_SPECS (e.g. 'gemini')
* @param {Record<string, string|undefined>} env - Environment variables
* @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise<Buffer | null>))|null }} [deps={}] - Injected dependencies
* @returns {import('./index').ProviderAdapter}
*/
function createGoogleProviderAdapter(providerKey, env, deps = {}) {
const spec = GOOGLE_PROVIDER_SPECS[providerKey];
if (!spec) {
throw new Error(`Unknown Google provider spec: ${providerKey}`);
}

const keyEnvVar = spec.envConstants.KEY;

return createGoogleApiKeyAdapter(env, deps, {
name: spec.name,
port: spec.port,
envConstants: spec.envConstants,
defaultTarget: spec.defaultTarget,
validationPath: spec.validationPath,
modelsPath: spec.modelsPath,
healthServiceName: `awf-api-proxy-${spec.name}`,
unconfiguredErrorMessage: `${spec.label} proxy not configured (no ${keyEnvVar}). Set ${keyEnvVar} in the AWF runner environment to enable credential isolation.`,
healthErrorMessage: `${keyEnvVar} not configured in api-proxy sidecar`,
...(spec.transformRequestUrl !== undefined ? { transformRequestUrl: spec.transformRequestUrl } : {}),
});
}

module.exports = { createGoogleApiKeyAdapter, createGoogleProviderAdapter };
63 changes: 63 additions & 0 deletions containers/api-proxy/providers/google-adapter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const { createGoogleProviderAdapter } = require('./google-adapter');
const { GOOGLE_PROVIDER_SPECS } = require('./google-provider-specs');
const { createGeminiAdapter } = require('./gemini');
const { createVertexAdapter } = require('./vertex');

describe('createGoogleProviderAdapter', () => {
it('throws for an unknown provider key', () => {
expect(() => createGoogleProviderAdapter('bogus', {})).toThrow(/Unknown Google provider spec: bogus/);
});

it('derives gemini ports, targets and messages from the spec', () => {
const adapter = createGoogleProviderAdapter('gemini', {});
expect(adapter.name).toBe('gemini');
expect(adapter.port).toBe(10003);
expect(adapter.isEnabled()).toBe(false);
expect(adapter.getUnconfiguredResponse()).toEqual({
statusCode: 503,
body: { error: 'Gemini proxy not configured (no GEMINI_API_KEY). Set GEMINI_API_KEY in the AWF runner environment to enable credential isolation.' },
});
expect(adapter.getUnconfiguredHealthResponse().body).toMatchObject({
service: 'awf-api-proxy-gemini',
error: 'GEMINI_API_KEY not configured in api-proxy sidecar',
});
});

it('derives vertex ports, targets and messages from the spec', () => {
const adapter = createGoogleProviderAdapter('vertex', {});
expect(adapter.name).toBe('vertex');
expect(adapter.port).toBe(10004);
expect(adapter.getUnconfiguredResponse()).toEqual({
statusCode: 503,
body: { error: 'Vertex AI proxy not configured (no GOOGLE_API_KEY). Set GOOGLE_API_KEY in the AWF runner environment to enable credential isolation.' },
});
expect(adapter.getUnconfiguredHealthResponse().body).toMatchObject({
service: 'awf-api-proxy-vertex',
error: 'GOOGLE_API_KEY not configured in api-proxy sidecar',
});
});

it('applies the gemini URL transform and omits it for vertex', () => {
const gemini = createGeminiAdapter({ GEMINI_API_KEY: 'k' });
const vertex = createVertexAdapter({ GOOGLE_API_KEY: 'k' });
expect(gemini.transformRequestUrl('/v1beta/models?key=secret')).toBe('/v1beta/models');
expect(vertex.transformRequestUrl).toBeUndefined();
});

it('exposes a models fetch config only when the spec defines a models path', () => {
const gemini = createGeminiAdapter({ GEMINI_API_KEY: 'k' });
const vertex = createVertexAdapter({ GOOGLE_API_KEY: 'k' });
expect(gemini.getModelsFetchConfig()).toMatchObject({
url: 'https://generativelanguage.googleapis.com/v1beta/models',
});
expect(vertex.getModelsFetchConfig()).toBeNull();
expect(GOOGLE_PROVIDER_SPECS.vertex.modelsPath).toBeNull();
});

it('authenticates both providers with the x-goog-api-key header', () => {
expect(createGeminiAdapter({ GEMINI_API_KEY: 'g' }).getAuthHeaders()).toEqual({ 'x-goog-api-key': 'g' });
expect(createVertexAdapter({ GOOGLE_API_KEY: 'v' }).getAuthHeaders()).toEqual({ 'x-goog-api-key': 'v' });
});
});
58 changes: 58 additions & 0 deletions containers/api-proxy/providers/google-provider-specs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

/**
* Declarative specs for the Google API-key–based providers (Gemini, Vertex AI).
*
* Centralize Google-backed provider adapter settings here and keep each wrapper
* thin. New providers must also follow the registration and wiring checklist in ADDING-A-PROVIDER.md.
*/

const { stripGeminiKeyParam } = require('../proxy-utils');
const { GEMINI_ENV, VERTEX_ENV } = require('../provider-env-constants');

/**
* @typedef {Object} GoogleProviderSpec
* @property {string} name - Provider slug (e.g. 'gemini')
* @property {string} label - Human-readable name used in error messages
* @property {number} port - Proxy port
* @property {{ KEY: string, TARGET: string, BASE_PATH: string }} envConstants - Env var name constants
* @property {string} defaultTarget - Default upstream hostname
* @property {string} validationPath - URL path for the health/validation probe
* @property {string|null} modelsPath - URL path for models fetch, or null if unsupported
* @property {((url: string) => string)} [transformRequestUrl] - Optional URL transformer
*/

/** @type {Record<string, GoogleProviderSpec>} */
const GOOGLE_PROVIDER_SPECS = {
gemini: {
name: 'gemini',
label: 'Gemini',
port: 10003,
envConstants: GEMINI_ENV,
defaultTarget: 'generativelanguage.googleapis.com',
validationPath: '/v1beta/models',
modelsPath: '/v1beta/models',
/**
* Strip Gemini SDK auth query parameters before forwarding.
* The SDK injects ?key= (or ?apiKey=, ?api_key=) alongside the header;
* forwarding both causes API_KEY_INVALID errors on the upstream.
*
* @param {string} url
* @returns {string}
*/
transformRequestUrl(url) {
return stripGeminiKeyParam(url);
},
},
vertex: {
name: 'vertex',
label: 'Vertex AI',
port: 10004,
envConstants: VERTEX_ENV,
defaultTarget: 'aiplatform.googleapis.com',
validationPath: '/v1/projects',
modelsPath: null,
},
};

module.exports = { GOOGLE_PROVIDER_SPECS };
17 changes: 4 additions & 13 deletions containers/api-proxy/providers/vertex.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
* (i.e. GOOGLE_GENAI_USE_VERTEXAI=true). Setting GOOGLE_VERTEX_BASE_URL routes
* all Vertex AI traffic through the api-proxy sidecar instead of calling
* aiplatform.googleapis.com directly, enabling credential isolation.
*
* All configuration lives in GOOGLE_PROVIDER_SPECS.vertex (google-provider-specs.js).
*/

const { VERTEX_ENV } = require('../provider-env-constants');
const { createGoogleApiKeyAdapter } = require('./google-adapter');
const { createGoogleProviderAdapter } = require('./google-adapter');

/**
* Create the Google Vertex AI provider adapter.
Expand All @@ -26,17 +27,7 @@ const { createGoogleApiKeyAdapter } = require('./google-adapter');
* @returns {import('./index').ProviderAdapter}
*/
function createVertexAdapter(env, deps = {}) {
return createGoogleApiKeyAdapter(env, deps, {
name: 'vertex',
port: 10004,
envConstants: VERTEX_ENV,
defaultTarget: 'aiplatform.googleapis.com',
validationPath: '/v1/projects',
modelsPath: null,
healthServiceName: 'awf-api-proxy-vertex',
unconfiguredErrorMessage: 'Vertex AI proxy not configured (no GOOGLE_API_KEY). Set GOOGLE_API_KEY in the AWF runner environment to enable credential isolation.',
healthErrorMessage: 'GOOGLE_API_KEY not configured in api-proxy sidecar',
});
return createGoogleProviderAdapter('vertex', env, deps);
}

module.exports = { createVertexAdapter };
2 changes: 1 addition & 1 deletion docs/authentication-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,7 @@ OIDC authentication is configured via `apiProxy.auth` in the AWF config file or
| `containers/api-proxy/providers/openai.js` | OpenAI adapter — selects OIDC provider based on `AWF_AUTH_PROVIDER` |
| `containers/api-proxy/providers/anthropic.js` | Anthropic adapter — static `x-api-key` or WIF `Authorization: Bearer` |
| `containers/api-proxy/providers/copilot.js`, `copilot-auth.js`, `copilot-byok.js` | Copilot adapter — GitHub token, BYOK, and OIDC handling, `token`/`Bearer` prefix logic |
| `containers/api-proxy/providers/gemini.js`, `vertex.js`, `google-adapter.js` | Gemini and Vertex AI adapters — static `x-goog-api-key` only, no OIDC |
| `containers/api-proxy/providers/gemini.js`, `vertex.js`, `google-adapter.js`, `google-provider-specs.js` | Gemini and Vertex AI adapters (declarative specs) — static `x-goog-api-key` only, no OIDC |
| `containers/agent/setup-iptables.sh` | iptables rules for api-proxy routing |
| `containers/agent/entrypoint.sh` | Entrypoint token cleanup, capability drop |
| `containers/agent/api-proxy-health-check.sh` | Pre-flight credential isolation verification |
Expand Down
Loading