Fix: honor a Vertex AI Express Mode API key for Gemini model calls - #366
Open
AmaadMartin wants to merge 4 commits into
Open
Fix: honor a Vertex AI Express Mode API key for Gemini model calls#366AmaadMartin wants to merge 4 commits into
AmaadMartin wants to merge 4 commits into
Conversation
added 4 commits
July 30, 2026 22:51
geminiInitParams never gave an API key to @google/genai on the Vertex branch, and the SDK discards an environment key whenever project/location are also supplied, so Express Mode was unreachable for model calls and a GOOGLE_API_KEY-based deployment could never drive the agent's LLM. Resolve an express key (explicit, else GOOGLE_API_KEY) on the Vertex branch and drop project/location when one is in effect, since the SDK rejects a client that carries both.
apigeeToGeminiInitParams flipped vertexai on after calling geminiInitParams, so an apigee/vertex_ai/ model first ran the Gemini API branch and could resolve GOOGLE_GENAI_API_KEY / GEMINI_API_KEY into apiKey. That key re-entered geminiInitParams through super() and would now be read as an Express Mode key, flipping Apigee Vertex users from ADC to key auth. Decide the flag first and pass it in; errors and messages are unchanged.
…ient options Drop the two single-use helpers the first pass introduced. @google/genai's mutual-exclusion guard is `(options.project || options.location) && options.apiKey`, so explicitly-undefined fields do not trip it and both Vertex clients can pass a flat option object. Also drop the unreachable `location: this.location || 'global'` default on the live client: geminiInitParams throws on an empty location, and the SDK applies the same default itself while correctly skipping it in Express Mode. Raise the express-key notice from info to warn, since it reports a credential source that outranks the ambient Vertex target.
This was referenced Jul 31, 2026
Open
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
Problem: A Vertex AI Express Mode API key never reaches the model. On the Vertex branch
geminiInitParams()fillsproject/locationfromGOOGLE_CLOUD_PROJECT/GOOGLE_CLOUD_LOCATIONand bothapiClientandliveApiClientconstructnew GoogleGenAI({vertexai, project, location, httpOptions})without ever passingapiKey.@google/genaithen discards the ambient key itself — withproject/locationsupplied it hitselse if ((options.project || options.location) && envApiKey) { this.apiKey = undefined; }— so agent model calls always authenticate with ADC. Two concrete symptoms:new Gemini({model: 'gemini-2.5-flash', vertexai: true, apiKey: 'k'})throwsVertexAI project must be provided via constructor or GOOGLE_CLOUD_PROJECT environment variable.— Express Mode cannot even be constructed.GOOGLE_API_KEY-based deployment is a no-op for the LLM.dev/src/cli/deploy/deploy_utils.ts:103-105writesGOOGLE_GENAI_USE_VERTEXAI=1,GOOGLE_CLOUD_PROJECTandGOOGLE_CLOUD_LOCATIONinto every generated Dockerfile unconditionally, so the ambient project always beat the key.Today
GOOGLE_API_KEYis consumed only bygetExpressModeApiKey()(core/src/utils/vertex_ai_utils.ts) for the session and memory services, so the same environment gave those services Express Mode and the model ADC.Solution: resolve an Express Mode key on the Vertex branch of
geminiInitParams()(explicitapiKey, elseGOOGLE_API_KEYon Node) and skip project/location resolution when one is in effect. Both Vertex clients now passapiKeyalongsideproject/location; when a key is in play the other two areundefined, which does not trip the SDK's(options.project || options.location) && options.apiKeymutual-exclusion guard.vertexai: trueis still passed, soapiBackendstaysVERTEX_AIandpreprocessRequestkeeps leavinglabels/displayNamealone.Precedence (explicit beats ambient):
apiKeyproject/locationGOOGLE_API_KEYVertexAI project must be provided…(unchanged)Three intentional behaviour changes:
GOOGLE_API_KEYnow outranks an ambient project/location on the Vertex branch. This inverts@google/genai's own ambient tiebreak, deliberately: the generated Dockerfile always writes project and location, so under the SDK's ordering a key-based deployment could never drive the model — the bug this PR fixes. It also matches what ADK already does for the session/memory services ingetExpressModeApiKey(). Alogger.warnfires when a key is picked up while an ambient project/location is being ignored, and the escape hatches are to unsetGOOGLE_API_KEYor passproject/locationexplicitly (explicit still wins, pinned by a test). The most likely way to hit this by accident is anadk create.env:generateEnvFile()(dev/src/cli/cli_create.ts:163-178) emitsGOOGLE_API_KEY, thenGOOGLE_GENAI_USE_VERTEXAI=0, then project/location andGOOGLE_GENAI_USE_VERTEXAI=1when the user supplies both a key and a project+region.apiKeyis combined with an explicitproject/locationundervertexai: true. Previously the key was silently ignored. The message mirrorsgetExpressModeApiKey(); the two pre-existing project/location errors keep their exact text and still fire on every non-express path. Failing atnew Gemini(...)rather than at first use is the point — silently dropping a credential is what is being fixed.ApigeeLlmordering fix, required to prevent an unintended change.apigeeToGeminiInitParams()flippedvertexaion after callinggeminiInitParams, so anapigee/vertex_ai/…model first ran the Gemini API branch and could resolveGOOGLE_GENAI_API_KEY/GEMINI_API_KEYintoapiKey; that key re-enteredgeminiInitParamsthroughsuper(...)and would now be misread as an express key, flipping Apigee Vertex users from ADC to key auth. The provider is now settled before the call. Net Apigee behaviour, including every error message, is unchanged.Notes for review:
GOOGLE_API_KEYmeans "Express key" on the Vertex branch whileGEMINI_API_KEY/GOOGLE_GENAI_API_KEYkeep meaning "AI Studio key". That is deliberate and matchesgetExpressModeApiKey(), which readsGOOGLE_API_KEYonly. The Gemini API branch is untouched — a dedicated test pins thatGOOGLE_API_KEYdoes not leak into it.getExpressModeApiKey()is not reused: it gates ongetBooleanEnvVar('GOOGLE_GENAI_USE_VERTEXAI')rather than the already-resolvedparams.vertexai, so it would silently discard an explicit{vertexai: true, apiKey}, and it readsprocess.envunguarded, wheregeminiInitParamswraps env reads in!isBrowser(). It is left untouched.location: this.location || 'global'default is removed. It was unreachable —locationisprivate readonly, assigned only fromgeminiInitParams, which throws on an empty location — and it would have broken Express Mode, since passinglocation: 'global'beside anapiKeymakes the SDK throw. The SDK applies the identical default itself (if (!this.location && !this.apiKey) this.location = 'global') and correctly skips it in Express Mode.core/test/models/apigee_llm_test.tshad its inlineafterEachconverted to a sharedclearEnvbound to bothbeforeEachandafterEach, andGOOGLE_API_KEY/GEMINI_API_KEYadded to it;core/test/models/google_llm_test.tsgetsGOOGLE_API_KEYadded to the existingclearEnv. This is purely additive isolation — no assertion or fixture value was rewritten, and no existing test was modified or deleted. Without it, an ambientGOOGLE_API_KEY(a developer machine, or a reused vitest worker) silently flips the existing Vertex tests into Express Mode.core/src/index.tsorcore/src/common.ts.GeminiParamsgains no field; only its JSDoc changed.gh pr list --repo AmaadMartin/adk-js --state open --limit 300, thengh pr diff --name-onlyon every adjacent PR): no open PR lands this change. Fix: forward the Vertex AI express-mode API key into the Agent Engines client #268 forwards an express key into the Agent Engines client (@google-cloud/vertexai, disjoint files). Fix: accept GOOGLE_API_KEY in geminiInitParams soadk create.env files work #313 addsGOOGLE_API_KEYto the non-Vertexelsebranch, Fix: honor an explicitly passed vertexai: false in Gemini / ApigeeLlm #266 changes thevertexaiflag resolution, Feat: Derive the Gemini vertexai gate from GOOGLE_GENAI_USE_ENTERPRISE (adk-python parity) #267 swaps the env gate — three PRs touching adjacent hunks of the same function and mutually exclusive as stacking bases, so this branches frommain; the hunks are disjoint and any conflict is a trivial adjacent-line merge.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.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
13 new cases. 100% line and branch coverage on every changed line of
core/src/models/google_llm.tsandcore/src/models/apigee_llm.ts(measured with--coverage, then intersected with the diff's changed line ranges — 0 uncovered new statements, 0 uncovered new branches).core/test/models/google_llm_test.ts—geminiInitParams Vertex AI Express Mode: explicit key with no project/location; env key beating an ambient project/location; env key with no ambient target; explicit project/location beating an env key; the mutual-exclusion throw for key+project, key+location and key+both;GOOGLE_API_KEYnot leaking into the Gemini API branch; idempotency (an express result fed back in is unchanged, whichApigeeLlmrelies on viasuper(...)).Vertex AI client credentials: expressapiClientandliveApiClientoptions, and classic options stayingapiKey-free.core/test/models/apigee_llm_test.ts:apigee/vertex_ai/…still demands a project whenGEMINI_API_KEYorGOOGLE_GENAI_API_KEYis set (the regression test for change 3), and reachesVERTEX_AIviaGOOGLE_API_KEYwith no project.tests/integration/models/gemini_express_mode_test.ts(new): drives a realGeminithrough the real@google/genaiclient with onlyglobalThis.fetchstubbed, and asserts the outgoing request carriesx-goog-api-key: <key>and a URL with no/projects/segment — both for an explicit key and forGOOGLE_API_KEYwith an ambient project/location. This is what proves the key reaches the wire rather than just the constructor.Every new test was proven able to fail. Each mutation below was applied to the source, the suite re-run, and the source restored:
git checkout main -- core/src/models/google_llm.ts(full revert)VertexAI project must be provided via constructor or GOOGLE_CLOUD_PROJECT environment variable./expected 'https://us-central1-aiplatform.google…' not to contain '/projects/'apiKey: this.apiKeyfrom both Vertex client constructionsexpected undefined to be 'express-key'/Authentication is not set up. Please provide either a project and location, or an API key, or a custom base URL.!project && !locationguardexpected [Function] to throw an error/expected 'env-express-key' to be undefinedGOOGLE_CLOUD_PROJECTexpected 'env-project' to be undefined/Cannot specify project or location and an Express Mode API key.(idempotency)GOOGLE_API_KEYto the non-Vertexelsebranchexpected 'env-express-key' to be undefinedapigeeToGeminiInitParamsorderingexpected [Function] to throw an errorgoogle_llm.tsrevert, against the Apigee suiteVertexAI project must be provided via constructor or GOOGLE_CLOUD_PROJECT environment variable.Commands run locally on the pushed commit (all green):
npm run ts:checkreports pre-existing errors in the test tree onmain(e.g.core/test/models/google_llm_test.ts:221,:809) and reports zero errors from any line this PR adds.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
No real Vertex AI Express Mode key was available, so no live generation was performed — this is not a claim that step 1 produced model output. What was verified locally instead, against the built
core/dist/cjsbundle: step 2 (new Gemini({model, vertexai: true, apiKey})with no environment at all now constructs and reportsbackend=VERTEX_AI, where it previously threwVertexAI project must be provided…) and step 3 (the ADC path with no key present still constructs,backend=VERTEX_AI). Also verified: the SDK's actual behaviour at the pinned@google/genai@2.9.0(new GoogleGenAI({vertexai: true, apiKey: 'k', project: 'p'})throwsProject/location and API key are mutually exclusive in the client initializer.;{vertexai: true, apiKey: 'k'}keeps the key;{vertexai: true, project, location}with an ambientGOOGLE_API_KEYdiscards it), plus the wire-level integration test above.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.