Fix: forward the Vertex AI express-mode API key into the Agent Engines client - #268
Open
AmaadMartin wants to merge 3 commits into
Open
Fix: forward the Vertex AI express-mode API key into the Agent Engines client#268AmaadMartin wants to merge 3 commits into
AmaadMartin wants to merge 3 commits into
Conversation
added 3 commits
July 29, 2026 13:33
…s client
VertexAiSessionService and VertexAiMemoryBankService resolved an
express-mode API key and then constructed their transport with
new Client({project, location}). In express mode both are undefined, so
the underlying genai ApiClient threw 'Authentication is not set up' and
the resolved key was never read.
@google-cloud/vertexai's Client has no API key option, so express mode
now builds the genai ApiClient directly with the key handed to both
NodeAuth (which emits x-goog-api-key) and ApiClient (which uses it to
skip the projects/{p}/locations/{l} URL prefix). The non-express path
still goes through Client with identical arguments.
Adds unit coverage for createExpressModeApiClient and both branches of
createAgentEnginesClient, express-mode construction for both services,
and stubbed-transport tests asserting outgoing requests carry
x-goog-api-key with no projects/{p}/locations/{l} path prefix.
The exported AgentEnginesClientOptions interface had exactly one consumer in the same module and is not part of the public API surface.
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
(No public GitHub issue is tracked for this bug; described below instead. The
placeholders above are left as-is per the required template.)
Problem: Vertex AI express mode (an API key instead of a project/location
pair) is unusable in
adk-js.VertexAiSessionServiceandVertexAiMemoryBankServiceboth resolve the express-mode key viagetExpressModeApiKey(), store it onthis.expressModeApiKey— and then neverread it. Both construct their transport with
new Client({project: this.projectId, location: this.location}), and in expressmode those are
undefined, so the underlying@google/genaiApiClientthrowsfrom its constructor:
Reproduce (the env var the checked-in
getExpressModeApiKey()reads isGOOGLE_GENAI_USE_VERTEXAI, seecore/src/utils/vertex_ai_utils.ts):Solution: Add a single factory,
createAgentEnginesClient(), incore/src/utils/vertex_ai_utils.tsand call it from both services, so theexpress/non-express decision exists in exactly one place. It mirrors the Python
reference (
_get_api_clientinsrc/google/adk/sessions/vertex_ai_session_service.pyand
.../memory/vertex_ai_memory_bank_service.py), which likewise selects oneof two clients rather than merging the options:
@google/genaiApiClientdirectly with thekey;
project/locationare deliberately dropped, as Python does.new Client({project, location}).agentEnginesInternal, bytefor byte what the code does today.
The obvious one-line fix — passing
apiKeyintonew Client({...})— does notwork and was not attempted.
@google-cloud/vertexai@1.12.0'sClientconstructor is typed
{project?, location?, apiEndpoint?}and at runtime alwaysbuilds
new NodeAuth({googleAuthOptions: {...}}); it never reads anapiKeyoption (the string
apiKeydoes not occur in its publishedbuild/srcat all).So express mode has to construct the underlying client itself.
The key must be passed to both
NodeAuthandApiClient, and this is easyto get wrong:
ApiClient.getAuthHeaders()delegates entirely toclientOptions.auth, soNodeAuth.apiKeyis what emits thex-goog-api-keyheader, while
ApiClient.apiKeyis what satisfies the auth check and suppressesthe
projects/{p}/locations/{l}URL prefix (shouldPrependVertexProjectPath()returns
falsewhenclientOptions.apiKeyis set). Passing it to only one ofthem silently produces a broken client — there is a dedicated test for that
(see the mutation testing below). The express client also replicates the
vendor's
userAgentExtra: \vertex-genai-modules/${SDK_VERSION}`` so telemetrydoes not change between modes.
Cross-language parity, and where it was overridden. The observable
behaviour follows Python: which auth material is used, and that project/location
are dropped when a key is present. The mechanism differs because the JS vendor
SDK exposes no
api_keyoption, soadk-jsbuilds the genaiApiClientdirectly where Python calls
vertexai.Client(api_key=...). Local TS convention(naming, module layout) was kept.
Disclosed: one type suppression, at one site.
core/src/utils/vertex_ai_utils.tscontains exactly one cast:AgentEnginesis compiled against the@google/genaicopy that@google-cloud/vertexai@1.12.0resolves (1.52.0), whilecoreresolves@google/genai@2.9.0. The twoApiClientclasses are identical at runtime butnominally distinct to
tscbecause of the privatecustomBaseUrlfield, givingTS2345: ... Types have separate declarations of a private property 'customBaseUrl'.This is known debt, not a workaround of choice — I verified there is no
cast-free path today:
@google-cloud/vertexai@1.12.0declares"@google/genai": "^1.45.0"andcore/package.jsondeclares"^2.9.0"; no single version satisfies bothranges, so deduping means force-overriding one package onto an undeclared
major.
1.12.0is the latest published@google-cloud/vertexai(npm viewshows nonewer release built against genai 2.x).
@google-cloud/vertexaidoes not re-exportApiClient, so there is nosame-declaration import path.
Follow-up: the workspace-wide
@google/genaidedupe is already owned byseparate PRs (#226, #228). When one of those lands, this cast and its
AgentEnginesApiClientalias can be deleted outright. Deliberately not donehere: it would put
package.json/package-lock.jsonchurn in a bug fix andcollide with those PRs.
Collision check (required). Ran
gh pr list --repo AmaadMartin/adk-js --state open --limit 300and diffed everyplausibly adjacent PR. No open PR lands this change. Two overlap by file
but not by logic, in disjoint regions:
feat/shared-reasoning-engine-name-parser) touchescore/src/utils/vertex_ai_utils.ts,core/src/sessions/vertex_ai_session_service.tsand
core/test/utils/vertex_ai_utils_test.ts, but only addsparseReasoningEngineNameand rewritesgetReasoningEngineId. It does nottouch client construction.
fix/vertex-session-id-hardening) touchescore/src/sessions/vertex_ai_session_service.ts, but onlyparseAppName/session-id normalization.
I branched from
mainrather than stacking: this change is semanticallyindependent of both, and because #227 and #201 overlap different files there
is no single branch to stack on that resolves both. Heads-up for whoever merges
second: #227 and this PR both append to the end of
core/src/utils/vertex_ai_utils.tsandcore/test/utils/vertex_ai_utils_test.ts,so expect a trivial append-vs-append textual conflict there.
Known follow-up, deliberately out of scope.
VertexAiMemoryBankServicestill has no precondition check symmetric toVertexAiSessionService's'Either (Project ID and Location) or an expressModeApiKey is required.',so with neither key nor project/location it keeps surfacing the raw genai error.
Fixing it touches the same constructor and would conflict with this change.
Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Only targeted tests were run, on the exact pushed commit:
Coverage of the new code, measured over those four files:
Both rewritten service
elsebranches are covered too; the residual uncoveredregions reported for the two service files (
vertex_ai_session_service.ts432-444/469-477,
vertex_ai_memory_bank_service.ts503-505/516-520) arepre-existing and untouched by this change. No coverage-tool suppressions were
added, and no test was skipped, weakened, or deleted.
npm run ts:checkis pre-existing broken onmain(745 errors, all fromtest trees that cannot resolve
@google/adk). I measured it before and after:745 both ways, and zero errors in any file this PR touches under
core/src/.CI does not run
ts:check.CI status — read this before re-running
run-testsis green onubuntu-latestandmacos-latest. Thewindows-latestjob is red on a pre-existing, unrelated flake, and I amreporting that rather than papering over it.
I re-ran the Windows job three times. It failed each time — on a different,
unrelated suite:
tests/integration/app_loader/app_loader_test.tsTest timed out in 40000ms(passed on re-run, taking 63.8s)tests/integration/a2a/input_required/input_required_test.tsCLI exited prematurely with code 1tests/integration/a2a/basic/a2a_agent_test.tsCLI exited prematurely with code 1tests/integration/tools/run_skill_script_tool_test.ts(PowerShell cases)Test timed out in 5000msIn every one of those four runs all 100 tests in the four suites this PR
touches passed on Windows, e.g.
✓ unit:core core/test/sessions/vertex_ai_session_service_test.ts (54 tests),and the whole matrix was green on Linux and macOS. The rest of the Windows run
was 213 passed / 1 failed each time.
I root-caused the a2a failures rather than assuming flakiness. The spawned test
server prints:
BaseTestServer.getRandomPort()intests/integration/test_case_utils.tsis40000 + Math.floor(Math.random() * 10000)— range 40000-49999, which overlapsthe Windows dynamic/ephemeral range starting at 49152, where Windows reserves
random excluded blocks. Binding inside one yields
EACCES, and the port ispicked blindly with no bind-and-retry, so a different suite loses the lottery on
each run. Filed as separate follow-up work; deliberately not fixed here,
since it is unrelated test-harness churn and PRs #235/#247/#256/#260 (app_loader
timeouts) and #210/#224/#233/#254 (Windows PowerShell/subprocess timeouts)
already contend over these same files.
Evidence this is not caused by this change: all three a2a suites pass locally on
this branch (
Test Files 3 passed | Tests 6 passed); the failures areEACCESon a socket bind and child-process timeouts, and this diff contains no
networking, server, port, or process code; and the failing suite changes every
run while its sibling a2a suites pass in the same run.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Proof the new tests can fail (mutation testing). Every new test was run
against mutated source and confirmed to FAIL:
createAgentEnginesClientdown the oldnew Client({project, location}).agentEnginesInternalpath for the express caseAuthentication is not set up. Please provide either a project and location, or an API key, or a custom base URL.new NodeAuth({apiKey})→new NodeAuth({googleAuthOptions: {scopes: [...]}})expected null to be 'fake-express-key'userAgentExtrafrom the express clientexpected undefined to be 'vertex-genai-modules/1.12.0'apiKeyfrom theApiClientoptions (leaving it onNodeAuth)Authentication is not set up. ...Mutations 2 and 4 are the important pair: they prove the tests pin the
both-places requirement rather than just "the constructor didn't throw".
New tests:
core/test/utils/vertex_ai_utils_test.ts—createExpressModeApiClientreports
getApiKey()/getProject()/getLocation()/isVertexAI()correctly,emits
x-goog-api-keyfromgetAuthHeaders(), and matches the vendor useragent;
createAgentEnginesClientcovers both branches plus theno-credentials throw (which pins that the non-express path is untouched).
core/test/sessions/vertex_ai_session_service_test.ts—new VertexAiSessionService({})underGOOGLE_GENAI_USE_VERTEXAI=true+GOOGLE_API_KEYno longer throws (the exactreported regression); same for an explicit
expressModeApiKey; and astubbed-
fetchtest drivingcreateSession()end to end that asserts the URLis
https://aiplatform.googleapis.com/v1beta1/reasoningEngines/12345/sessions— no
projects/.../locations/...prefix — withx-goog-api-keyon therequest.
core/test/memory/vertex_ai_memory_bank_service_test.ts— the same twoconstruction cases for the memory bank.
tests/integration/memory/vertex_ai_memory_bank_service_test.ts— express-modesearchMemory()over a stubbed transport, asserting.../v1beta1/reasoningEngines/test-engine-id/memories:retrieveand the keyheader. No network calls; consistent with the existing mock-injecting
integration tests in that file.
All tests use the fake key
'fake-express-key', setGOOGLE_GENAI_USE_VERTEXAIexplicitly, and restore
process.envinafterEach. The API key never appearsin a log line, error message, or user agent.
Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Not runnable in CI — needs a real express-mode key. To verify by hand:
Expected: the constructor no longer throws, a session is returned, and the
outbound request goes to
https://aiplatform.googleapis.com/v1beta1/reasoningEngines/<engine-id>/sessionswith an
x-goog-api-keyheader and noprojects/.../locations/...prefix.Before this change the constructor threw
Authentication is not set up. ....Behaviour with
projectId+locationis unchanged — sameClient, samearguments — and is covered by the pre-existing tests, which all still pass.
Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.