From b1eafb631a3a393980cda96f40899ca066df2a1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:50:22 +0000 Subject: [PATCH 1/3] Initial plan From fead23a351254e1ce97cfff76dd61b035d4b5025 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:55:06 +0000 Subject: [PATCH 2/3] refactor: extract createGoogleApiKeyAdapter factory for Gemini and Vertex Both providers shared ~45 lines of identical Google API-key adapter scaffolding (createProviderAuthScaffold, x-goog-api-key header builder, createAdapterMethods, buildProviderAdapter, isEnabled, unconfigured responses). Extract to a new providers/google-adapter.js factory so each provider only supplies its name, port, env constants, paths, and error messages. Closes #5918 --- containers/api-proxy/providers/gemini.js | 49 ++-------- .../api-proxy/providers/google-adapter.js | 95 +++++++++++++++++++ containers/api-proxy/providers/vertex.js | 49 ++-------- 3 files changed, 110 insertions(+), 83 deletions(-) create mode 100644 containers/api-proxy/providers/google-adapter.js diff --git a/containers/api-proxy/providers/gemini.js b/containers/api-proxy/providers/gemini.js index 9a073311..6cf580e4 100644 --- a/containers/api-proxy/providers/gemini.js +++ b/containers/api-proxy/providers/gemini.js @@ -13,10 +13,9 @@ * Gemini SDK versions append alongside the header. */ -const { stripGeminiKeyParam, makeUnconfiguredHealthResponse } = require('../proxy-utils'); -const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); +const { stripGeminiKeyParam } = require('../proxy-utils'); const { GEMINI_ENV } = require('../provider-env-constants'); -const { providerKeyHeaders } = require('./auth-headers'); +const { createGoogleApiKeyAdapter } = require('./google-adapter'); /** * Create the Google Gemini provider adapter. @@ -26,37 +25,16 @@ const { providerKeyHeaders } = require('./auth-headers'); * @returns {import('./index').ProviderAdapter} */ function createGeminiAdapter(env, deps = {}) { - const { apiKey, rawTarget, basePath, bodyTransform } = createProviderAuthScaffold(env, deps, { - keyEnvVar: GEMINI_ENV.KEY, - targetEnvVar: GEMINI_ENV.TARGET, - basePathEnvVar: GEMINI_ENV.BASE_PATH, - defaultTarget: 'generativelanguage.googleapis.com', - }); - const buildAuthHeaders = () => providerKeyHeaders('x-goog-api-key', apiKey); - - const adapterMethods = createAdapterMethods({ - apiKey, - rawTarget, - basePath, - provider: 'gemini', + return createGoogleApiKeyAdapter(env, deps, { + name: 'gemini', port: 10003, + envConstants: GEMINI_ENV, defaultTarget: 'generativelanguage.googleapis.com', validationPath: '/v1beta/models', - validationHeaders: buildAuthHeaders, modelsPath: '/v1beta/models', - modelsFetchHeaders: buildAuthHeaders, - }); - - return buildProviderAdapter({ - name: 'gemini', - port: 10003, - isManagementPort: false, - adapterMethods, - getAuthHeaders() { - return buildAuthHeaders(); - }, - bodyTransform, - isEnabled() { return !!apiKey; }, + 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; @@ -68,17 +46,6 @@ function createGeminiAdapter(env, deps = {}) { transformRequestUrl(url) { return stripGeminiKeyParam(url); }, - /** Response returned for all requests when no GEMINI_API_KEY is configured. */ - getUnconfiguredResponse() { - return { - 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.' }, - }; - }, - /** /health response when not configured. */ - getUnconfiguredHealthResponse() { - return makeUnconfiguredHealthResponse('awf-api-proxy-gemini', 'GEMINI_API_KEY not configured in api-proxy sidecar'); - }, }); } diff --git a/containers/api-proxy/providers/google-adapter.js b/containers/api-proxy/providers/google-adapter.js new file mode 100644 index 00000000..198a3118 --- /dev/null +++ b/containers/api-proxy/providers/google-adapter.js @@ -0,0 +1,95 @@ +'use strict'; + +/** + * Shared factory for Google API-key–based provider adapters (Gemini, Vertex). + * + * Both providers authenticate via the `x-goog-api-key` header and share the + * same scaffold: createProviderAuthScaffold, createAdapterMethods, and + * buildProviderAdapter. This factory centralises that boilerplate so each + * provider only supplies its name, port, env constants, target, paths, and + * error messages. + */ + +const { makeUnconfiguredHealthResponse } = require('../proxy-utils'); +const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); +const { providerKeyHeaders } = require('./auth-headers'); + +/** + * Create a Google API-key–based provider adapter. + * + * @param {Record} env - Environment variables + * @param {{ bodyTransform?: ((body: Buffer) => Buffer|null)|null }} [deps={}] - Injected dependencies + * @param {object} opts + * @param {string} opts.name - Provider slug (e.g. 'gemini') + * @param {number} opts.port - Proxy port (e.g. 10003) + * @param {{ KEY: string, TARGET: string, BASE_PATH: string }} opts.envConstants - Env var name constants + * @param {string} opts.defaultTarget - Default upstream hostname + * @param {string} opts.validationPath - URL path for health/validation probe + * @param {string|null} opts.modelsPath - URL path for models fetch, or null if unsupported + * @param {string} opts.healthServiceName - Service name for health response (e.g. 'awf-api-proxy-gemini') + * @param {string} opts.unconfiguredErrorMessage - Error body when no API key is configured + * @param {string} opts.healthErrorMessage - Health error message when not configured + * @param {((url: string) => string)} [opts.transformRequestUrl] - Optional URL transformer + * @returns {import('./index').ProviderAdapter} + */ +function createGoogleApiKeyAdapter(env, deps = {}, opts) { + const { + name, + port, + envConstants, + defaultTarget, + validationPath, + modelsPath, + healthServiceName, + unconfiguredErrorMessage, + healthErrorMessage, + transformRequestUrl, + } = opts; + + const { apiKey, rawTarget, basePath, bodyTransform } = createProviderAuthScaffold(env, deps, { + keyEnvVar: envConstants.KEY, + targetEnvVar: envConstants.TARGET, + basePathEnvVar: envConstants.BASE_PATH, + defaultTarget, + }); + const buildAuthHeaders = () => providerKeyHeaders('x-goog-api-key', apiKey); + + const adapterMethods = createAdapterMethods({ + apiKey, + rawTarget, + basePath, + provider: name, + port, + defaultTarget, + validationPath, + validationHeaders: buildAuthHeaders, + modelsPath, + modelsFetchHeaders: modelsPath ? buildAuthHeaders : null, + }); + + return buildProviderAdapter({ + name, + port, + isManagementPort: false, + adapterMethods, + getAuthHeaders() { + return buildAuthHeaders(); + }, + bodyTransform, + isEnabled() { return !!apiKey; }, + ...(transformRequestUrl !== undefined ? { transformRequestUrl } : {}), + /** Response returned for all requests when no API key is configured. */ + getUnconfiguredResponse() { + return { + statusCode: 503, + body: { error: unconfiguredErrorMessage }, + }; + }, + /** /health response when not configured. */ + getUnconfiguredHealthResponse() { + return makeUnconfiguredHealthResponse(healthServiceName, healthErrorMessage); + }, + }); +} + +module.exports = { createGoogleApiKeyAdapter }; diff --git a/containers/api-proxy/providers/vertex.js b/containers/api-proxy/providers/vertex.js index dd1dac6b..4c920e55 100644 --- a/containers/api-proxy/providers/vertex.js +++ b/containers/api-proxy/providers/vertex.js @@ -15,10 +15,8 @@ * aiplatform.googleapis.com directly, enabling credential isolation. */ -const { makeUnconfiguredHealthResponse } = require('../proxy-utils'); -const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); const { VERTEX_ENV } = require('../provider-env-constants'); -const { providerKeyHeaders } = require('./auth-headers'); +const { createGoogleApiKeyAdapter } = require('./google-adapter'); /** * Create the Google Vertex AI provider adapter. @@ -28,49 +26,16 @@ const { providerKeyHeaders } = require('./auth-headers'); * @returns {import('./index').ProviderAdapter} */ function createVertexAdapter(env, deps = {}) { - const { apiKey, rawTarget, basePath, bodyTransform } = createProviderAuthScaffold(env, deps, { - keyEnvVar: VERTEX_ENV.KEY, - targetEnvVar: VERTEX_ENV.TARGET, - basePathEnvVar: VERTEX_ENV.BASE_PATH, - defaultTarget: 'aiplatform.googleapis.com', - }); - const buildAuthHeaders = () => providerKeyHeaders('x-goog-api-key', apiKey); - - const adapterMethods = createAdapterMethods({ - apiKey, - rawTarget, - basePath, - provider: 'vertex', + return createGoogleApiKeyAdapter(env, deps, { + name: 'vertex', port: 10004, + envConstants: VERTEX_ENV, defaultTarget: 'aiplatform.googleapis.com', validationPath: '/v1/projects', - validationHeaders: buildAuthHeaders, modelsPath: null, - modelsFetchHeaders: null, - }); - - return buildProviderAdapter({ - name: 'vertex', - port: 10004, - isManagementPort: false, - alwaysBind: true, - adapterMethods, - getAuthHeaders() { - return buildAuthHeaders(); - }, - bodyTransform, - isEnabled() { return !!apiKey; }, - /** Response returned for all requests when no GOOGLE_API_KEY is configured. */ - getUnconfiguredResponse() { - return { - 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.' }, - }; - }, - /** /health response when not configured. */ - getUnconfiguredHealthResponse() { - return makeUnconfiguredHealthResponse('awf-api-proxy-vertex', 'GOOGLE_API_KEY not configured in api-proxy sidecar'); - }, + 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', }); } From c752a8027d9d1ad70726700faea84fc8bcdf7b11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:23:04 +0000 Subject: [PATCH 3/3] test(api-proxy): address google adapter review feedback --- containers/api-proxy/providers/gemini.js | 2 +- .../api-proxy/providers/google-adapter.js | 2 +- containers/api-proxy/providers/vertex.js | 2 +- .../api-proxy/server.auth-matrix.test.js | 34 +++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/containers/api-proxy/providers/gemini.js b/containers/api-proxy/providers/gemini.js index 6cf580e4..cebbdba1 100644 --- a/containers/api-proxy/providers/gemini.js +++ b/containers/api-proxy/providers/gemini.js @@ -21,7 +21,7 @@ const { createGoogleApiKeyAdapter } = require('./google-adapter'); * Create the Google Gemini provider adapter. * * @param {Record} env - Environment variables - * @param {{ bodyTransform: ((body: Buffer) => Buffer|null)|null }} deps - Injected dependencies + * @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise))|null }} [deps={}] - Injected dependencies * @returns {import('./index').ProviderAdapter} */ function createGeminiAdapter(env, deps = {}) { diff --git a/containers/api-proxy/providers/google-adapter.js b/containers/api-proxy/providers/google-adapter.js index 198a3118..272a424a 100644 --- a/containers/api-proxy/providers/google-adapter.js +++ b/containers/api-proxy/providers/google-adapter.js @@ -18,7 +18,7 @@ const { providerKeyHeaders } = require('./auth-headers'); * Create a Google API-key–based provider adapter. * * @param {Record} env - Environment variables - * @param {{ bodyTransform?: ((body: Buffer) => Buffer|null)|null }} [deps={}] - Injected dependencies + * @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise))|null }} [deps={}] - Injected dependencies * @param {object} opts * @param {string} opts.name - Provider slug (e.g. 'gemini') * @param {number} opts.port - Proxy port (e.g. 10003) diff --git a/containers/api-proxy/providers/vertex.js b/containers/api-proxy/providers/vertex.js index 4c920e55..eb0f47d9 100644 --- a/containers/api-proxy/providers/vertex.js +++ b/containers/api-proxy/providers/vertex.js @@ -22,7 +22,7 @@ const { createGoogleApiKeyAdapter } = require('./google-adapter'); * Create the Google Vertex AI provider adapter. * * @param {Record} env - Environment variables - * @param {{ bodyTransform: ((body: Buffer) => Buffer|null)|null }} deps - Injected dependencies + * @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise))|null }} [deps={}] - Injected dependencies * @returns {import('./index').ProviderAdapter} */ function createVertexAdapter(env, deps = {}) { diff --git a/containers/api-proxy/server.auth-matrix.test.js b/containers/api-proxy/server.auth-matrix.test.js index b342e071..7efdfdd8 100644 --- a/containers/api-proxy/server.auth-matrix.test.js +++ b/containers/api-proxy/server.auth-matrix.test.js @@ -14,6 +14,7 @@ const { createOpenAIAdapter } = require('./providers/openai'); const { createAnthropicAdapter } = require('./providers/anthropic'); const { createCopilotAdapter } = require('./providers/copilot'); const { createGeminiAdapter } = require('./providers/gemini'); +const { createVertexAdapter } = require('./providers/vertex'); // --------------------------------------------------------------------------- // Helpers @@ -523,6 +524,39 @@ describe('Auth Matrix — Gemini', () => { }); }); +// --------------------------------------------------------------------------- +// Vertex Auth Matrix +// --------------------------------------------------------------------------- + +describe('Auth Matrix — Vertex', () => { + describe('static API key', () => { + it('sends x-goog-api-key header', () => { + const adapter = createVertexAdapter({ GOOGLE_API_KEY: 'AIza-test-key' }); + const headers = adapter.getAuthHeaders(fakeReq()); + expect(headers['x-goog-api-key']).toBe('AIza-test-key'); + expect(headers.Authorization).toBeUndefined(); + }); + + it('targets aiplatform.googleapis.com by default', () => { + const adapter = createVertexAdapter({ GOOGLE_API_KEY: 'AIza-test' }); + const probe = adapter.getValidationProbe(); + expect(probe.url).toContain('aiplatform.googleapis.com'); + }); + }); + + describe('isEnabled gating', () => { + it('is disabled when no API key set', () => { + const adapter = createVertexAdapter({}); + expect(adapter.isEnabled()).toBe(false); + }); + + it('is enabled when API key is set', () => { + const adapter = createVertexAdapter({ GOOGLE_API_KEY: 'AIza-test' }); + expect(adapter.isEnabled()).toBe(true); + }); + }); +}); + // --------------------------------------------------------------------------- // Cross-cutting: credential isolation // ---------------------------------------------------------------------------