From 82a2f56c4e37c45354180f524e2586784e7661bf Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Mon, 3 Aug 2026 21:25:54 -0700 Subject: [PATCH 1/4] fix(utils): match only gemini-1. in isGemini1Model isGemini1Model used a `startsWith('gemini-1')` prefix test, so `gemini-1`, `gemini-1-pro`, `gemini-1.` and a future double-digit major such as `gemini-10.0-pro` were all classified as Gemini 1.x. adk-python matches `^gemini-1\.\d+` (src/google/adk/utils/model_name_utils.py), which requires the dotted minor version. Four built-in tools branch on this predicate, so a `gemini-10` model would have been routed down the legacy Gemini 1.x path. --- core/src/utils/model_name.ts | 11 ++++++--- core/test/utils/model_name_test.ts | 37 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/core/src/utils/model_name.ts b/core/src/utils/model_name.ts index a1d653294..464ec51d7 100644 --- a/core/src/utils/model_name.ts +++ b/core/src/utils/model_name.ts @@ -25,6 +25,13 @@ const MODELS_PREFIX = 'models/'; const EAP_MODEL_NAME_PATTERN = /^gemini-[a-z0-9_]+(?:-[a-z0-9_]+)*-early-exp\d*$/; +/** + * Matches Gemini 1.x names such as `gemini-1.5-pro`. The dotted minor version + * is mandatory, so a future double-digit major like `gemini-10.0-pro` is not + * mistaken for Gemini 1.x. + */ +const GEMINI_1_MODEL_NAME_PATTERN = /^gemini-1\.\d+/; + /** * Extract the actual model name from a simple, path-based, `models/`-prefixed * or provider-prefixed model string. @@ -112,9 +119,7 @@ function parseVersion(versionString: string): ParsedVersion { * @return true if it's a Gemini 1.x model, false otherwise. */ export function isGemini1Model(modelString: string): boolean { - const modelName = extractModelName(modelString); - - return modelName.startsWith('gemini-1'); + return GEMINI_1_MODEL_NAME_PATTERN.test(extractModelName(modelString)); } /** diff --git a/core/test/utils/model_name_test.ts b/core/test/utils/model_name_test.ts index aee05e796..c599700d4 100644 --- a/core/test/utils/model_name_test.ts +++ b/core/test/utils/model_name_test.ts @@ -178,6 +178,43 @@ describe('isGemini1Model', () => { expect(isGemini1Model('gemini/gemini-2.5-flash')).toBe(false); }); }); + + describe('version boundary', () => { + const gemini1Models = [ + 'gemini-1.5-flash', + 'gemini-1.0-pro', + 'gemini-1.5-pro-preview', + 'gemini-1.9-experimental', + 'projects/12345/locations/us-east1/publishers/google/models/gemini-1.0-pro-preview', + 'gemini/gemini-1.5-flash', + ]; + + for (const model of gemini1Models) { + it(`should return true for model: ${model}`, () => { + expect(isGemini1Model(model)).toBe(true); + }); + } + + const nonGemini1Models = [ + // A double-digit major must not be read as Gemini 1.x. + 'gemini-10.0-pro', + 'gemini-10-flash', + // The dotted minor version is mandatory. + 'gemini-1', + 'gemini-1-pro', + 'gemini-1.', + 'gemini-2.5-flash', + 'claude-3-sonnet', + 'my-gemini-1.5-model', + '', + ]; + + for (const model of nonGemini1Models) { + it(`should return false for model: ${model || ''}`, () => { + expect(isGemini1Model(model)).toBe(false); + }); + } + }); }); describe('isGemini2OrAbove', () => { From 019349a5fb829ab5a818d9af33546daeb8aea57c Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Mon, 3 Aug 2026 21:36:40 -0700 Subject: [PATCH 2/4] refactor(utils): split the EAP check out into isGeminiEapOr2OrAbove isGemini2OrAbove had the EAP naming test folded into it, which widened all three of its call sites at once. adk-python keeps the two concerns apart: the built-in code executor and the URL context tool call is_gemini_eap_or_2_or_above, while the CFC gate in runners.py uses a bare startswith('gemini-2') and rejects EAP ids. Restore isGemini2OrAbove to numeric-version semantics, add isGeminiEapOr2OrAbove alongside it, and migrate only the two call sites Python routes through the EAP-aware predicate. The runner CFC gate keeps calling isGemini2OrAbove, so it now matches Python again. The thrown messages at both migrated call sites are unchanged. No public export is added, renamed, or removed: isGeminiEapOr2OrAbove is internal to the package. The EAP test cases move from the isGemini2OrAbove describe block to the new predicate with their inputs and expectations intact, and a new block pins isGemini2OrAbove returning false for EAP ids so the split cannot be silently undone. New Runner CFC tests assert the gate rejects an EAP id. --- .../code_executors/built_in_code_executor.ts | 8 +- core/src/tools/url_context_tool.ts | 8 +- core/src/utils/model_name.ts | 42 ++++-- core/test/runner/runner_test.ts | 60 +++++++++ core/test/tools/url_context_tool_test.ts | 46 +++++++ core/test/utils/model_name_test.ts | 125 +++++++++++++++++- 6 files changed, 266 insertions(+), 23 deletions(-) diff --git a/core/src/code_executors/built_in_code_executor.ts b/core/src/code_executors/built_in_code_executor.ts index b8a7ee014..4b3a0ebe8 100644 --- a/core/src/code_executors/built_in_code_executor.ts +++ b/core/src/code_executors/built_in_code_executor.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import {LlmRequest} from '../models/llm_request.js'; -import {isGemini2OrAbove} from '../utils/model_name.js'; +import {isGeminiEapOr2OrAbove} from '../utils/model_name.js'; import {BaseCodeExecutor, ExecuteCodeParams} from './base_code_executor.js'; import {CodeExecutionResult} from './code_execution_utils.js'; @@ -36,8 +36,8 @@ export function isBuiltInCodeExecutor( /** * A code executor that uses the Model's built-in code executor. * - * Currently only supports Gemini 2.0+ models, but will be expanded to - * other models. + * Currently only supports Gemini 2.0+ and Gemini EAP models, but will be + * expanded to other models. */ export class BuiltInCodeExecutor extends BaseCodeExecutor { /** A unique symbol to identify BuiltInCodeExecutor class. */ @@ -52,7 +52,7 @@ export class BuiltInCodeExecutor extends BaseCodeExecutor { } processLlmRequest(llmRequest: LlmRequest) { - if (llmRequest.model && isGemini2OrAbove(llmRequest.model)) { + if (llmRequest.model && isGeminiEapOr2OrAbove(llmRequest.model)) { llmRequest.config = llmRequest.config || {}; llmRequest.config.tools = llmRequest.config.tools || []; llmRequest.config.tools.push({codeExecution: {}}); diff --git a/core/src/tools/url_context_tool.ts b/core/src/tools/url_context_tool.ts index 0388acfc6..b80a120b2 100644 --- a/core/src/tools/url_context_tool.ts +++ b/core/src/tools/url_context_tool.ts @@ -5,13 +5,13 @@ */ import {GenerateContentConfig} from '@google/genai'; -import {isGemini2OrAbove, isGeminiModel} from '../utils/model_name.js'; +import {isGeminiEapOr2OrAbove, isGeminiModel} from '../utils/model_name.js'; import {BaseTool, ToolProcessLlmRequest} from './base_tool.js'; /** - * A built-in tool that allows Gemini 2+ models to retrieve content from URLs - * provided in the conversation. + * A built-in tool that allows Gemini 2+ and Gemini EAP models to retrieve + * content from URLs provided in the conversation. * * This tool operates internally within the model and does not require or * perform local code execution. @@ -40,7 +40,7 @@ export class UrlContextTool extends BaseTool { ); } - if (!isGemini2OrAbove(llmRequest.model)) { + if (!isGeminiEapOr2OrAbove(llmRequest.model)) { throw new Error( `URL context tool requires Gemini 2 or above, but got ${llmRequest.model}`, ); diff --git a/core/src/utils/model_name.ts b/core/src/utils/model_name.ts index 464ec51d7..8dfb934a1 100644 --- a/core/src/utils/model_name.ts +++ b/core/src/utils/model_name.ts @@ -123,17 +123,13 @@ export function isGemini1Model(modelString: string): boolean { } /** - * Check if the model is a Gemini EAP or a Gemini 2.0+ model. + * Check if the model is a Gemini 2.x model using regex patterns. * - * EAP Gemini models do not encode a numeric version, so they are matched - * first by their naming convention — `gemini--early-exp` with an - * optional numeric suffix, e.g. `gemini-flash-early-exp` or - * `gemini-flash-early-exp3`. Otherwise the model name is parsed as a version - * and matches when the major version is >= 2. + * EAP models are deliberately not matched here: they carry no numeric version. + * Use {@link isGeminiEapOr2OrAbove} where they should be accepted. * * @param modelString Either a simple model name or path - based model name - * @return true if it's a Gemini EAP model or a Gemini 2.0+ model, false - * otherwise. + * @return true if it's a Gemini 2.x model, false otherwise. */ export function isGemini2OrAbove(modelString: string): boolean { if (!modelString) { @@ -142,10 +138,6 @@ export function isGemini2OrAbove(modelString: string): boolean { const modelName = extractModelName(modelString); - if (EAP_MODEL_NAME_PATTERN.test(modelName)) { - return true; - } - if (!modelName.startsWith('gemini-')) { return false; } @@ -156,6 +148,32 @@ export function isGemini2OrAbove(modelString: string): boolean { return parsedVersion.valid && parsedVersion.major >= 2; } +/** + * Check if the model is an Early Access Program (EAP) Gemini model, i.e. + * `gemini--early-exp` with an optional numeric suffix — for example + * `gemini-flash-early-exp` or `gemini-flash-lite-early-exp3`. + * + * @param modelString Either a simple model name or path - based model name + * @return true if it's a Gemini EAP model, false otherwise. + */ +function isGeminiEapModel(modelString: string): boolean { + return EAP_MODEL_NAME_PATTERN.test(extractModelName(modelString)); +} + +/** + * Check if the model is a Gemini EAP or a Gemini 2.0+ model. + * + * EAP Gemini models do not encode a numeric version, so they are matched by + * naming convention before any version parsing. + * + * @param modelString Either a simple model name or path - based model name + * @return true if it's a Gemini EAP model or a Gemini 2.0+ model, false + * otherwise. + */ +export function isGeminiEapOr2OrAbove(modelString: string): boolean { + return isGeminiEapModel(modelString) || isGemini2OrAbove(modelString); +} + /** * Check if the model is a Gemini 3.x Flash Live model. * diff --git a/core/test/runner/runner_test.ts b/core/test/runner/runner_test.ts index 904db5e3f..266479b37 100644 --- a/core/test/runner/runner_test.ts +++ b/core/test/runner/runner_test.ts @@ -7,6 +7,8 @@ import { App, BaseAgent, + BaseLlm, + BaseLlmConnection, BasePlugin, createEvent, createResumabilityConfig, @@ -17,6 +19,7 @@ import { InvocationContext, isRoutableLlmAgent, LlmAgent, + LlmResponse, Runner, } from '@google/adk'; import {Content, FunctionCall, FunctionResponse} from '@google/genai'; @@ -1241,3 +1244,60 @@ describe('Runner artifact saving (`saveInputBlobsAsArtifacts`)', () => { ]); }); }); + +/** + * A model stub carrying only an id, for gates that inspect the model name + * before any request is issued. + */ +class NamedModelStub extends BaseLlm { + async *generateContentAsync(): AsyncGenerator { + yield {content: {role: 'model', parts: [{text: ''}]}}; + } + + connect(): Promise { + return Promise.reject(new Error('connect is not supported by this stub')); + } +} + +describe('Runner CFC model gate', () => { + const sessionService = new InMemorySessionService(); + + async function runWithCfc(model: string): Promise { + const runner = new Runner({ + appName: TEST_APP_ID, + agent: new LlmAgent({ + name: 'cfc_agent', + model: new NamedModelStub({model}), + }), + sessionService, + }); + const session = await sessionService.createSession({ + appName: TEST_APP_ID, + userId: TEST_USER_ID, + }); + + const events: Event[] = []; + for await (const event of runner.runAsync({ + userId: session.userId, + sessionId: session.id, + newMessage: {role: 'user', parts: [{text: TEST_MESSAGE}]}, + runConfig: {supportCfc: true}, + })) { + events.push(event); + } + + return events; + } + + it('rejects an EAP model, matching the adk-python bare gemini-2 prefix gate', async () => { + await expect(runWithCfc('gemini-flash-early-exp')).rejects.toThrow( + 'CFC is not supported for model: gemini-flash-early-exp in agent: cfc_agent', + ); + }); + + it('rejects a Gemini 1.x model', async () => { + await expect(runWithCfc('gemini-1.5-pro')).rejects.toThrow( + 'CFC is not supported for model: gemini-1.5-pro in agent: cfc_agent', + ); + }); +}); diff --git a/core/test/tools/url_context_tool_test.ts b/core/test/tools/url_context_tool_test.ts index 4bebba3bf..b1d05231f 100644 --- a/core/test/tools/url_context_tool_test.ts +++ b/core/test/tools/url_context_tool_test.ts @@ -87,6 +87,52 @@ describe('UrlContextTool', () => { expect(req.config!.tools).toEqual([{urlContext: {}}]); }); + const extendedForms = [ + 'models/gemini-2.5-pro', + 'gemini/gemini-2.5-flash', + 'apigee/vertex_ai/v1beta/gemini-2.5-flash', + 'models/gemini-flash-early-exp', + ]; + + for (const model of extendedForms) { + it(`adds urlContext for model: ${model}`, async () => { + const tool = new UrlContextTool(); + const req = makeRequest(model); + await tool.processLlmRequest({ + llmRequest: req, + toolContext: makeToolContext(), + }); + + expect(req.config!.tools).toEqual([{urlContext: {}}]); + }); + } + + const rejectedForms: Array<[string, string]> = [ + [ + 'openrouter/google/gemini-1.5-pro:online', + 'URL context tool requires Gemini 2 or above, but got openrouter/google/gemini-1.5-pro:online', + ], + [ + // Malformed Vertex path: the trailing segment must not be read as an + // id, so this stays a non-Gemini model. + 'projects/123/locations/us-central1/publishers/google/gemini-2.5-flash', + 'URL context tool is not supported for model projects/123/locations/us-central1/publishers/google/gemini-2.5-flash', + ], + ]; + + for (const [model, message] of rejectedForms) { + it(`throws for model: ${model}`, async () => { + const tool = new UrlContextTool(); + const req = makeRequest(model); + await expect( + tool.processLlmRequest({ + llmRequest: req, + toolContext: makeToolContext(), + }), + ).rejects.toThrow(message); + }); + } + it('throws for Gemini 1.x model', async () => { const tool = new UrlContextTool(); const req = makeRequest('gemini-1.5-pro'); diff --git a/core/test/utils/model_name_test.ts b/core/test/utils/model_name_test.ts index c599700d4..69500c591 100644 --- a/core/test/utils/model_name_test.ts +++ b/core/test/utils/model_name_test.ts @@ -9,6 +9,7 @@ import {describe, expect, it} from 'vitest'; import { extractModelName, isGemini1Model, + isGeminiEapOr2OrAbove, isGeminiModel, } from '../../src/utils/model_name.js'; @@ -264,6 +265,23 @@ describe('isGemini2OrAbove', () => { } }); + describe('EAP models', () => { + const eapModels = [ + 'gemini-flash-early-exp', + 'gemini-flash-early-exp3', + 'gemini-flash-lite-early-exp', + 'projects/my-project/locations/us-central1/publishers/google/models/gemini-flash-early-exp', + ]; + + for (const model of eapModels) { + it(`should return false for EAP model: ${model}`, () => { + expect(isGemini2OrAbove(model)).toBe(false); + }); + } + }); +}); + +describe('isGeminiEapOr2OrAbove', () => { describe('EAP models', () => { const eapModels = [ 'gemini-flash-early-exp', @@ -277,7 +295,7 @@ describe('isGemini2OrAbove', () => { for (const model of eapModels) { it(`should return true for EAP model: ${model}`, () => { - expect(isGemini2OrAbove(model)).toBe(true); + expect(isGeminiEapOr2OrAbove(model)).toBe(true); }); } @@ -293,15 +311,65 @@ describe('isGemini2OrAbove', () => { for (const model of nonEapModels) { it(`should return false for non-EAP model: ${model}`, () => { - expect(isGemini2OrAbove(model)).toBe(false); + expect(isGeminiEapOr2OrAbove(model)).toBe(false); }); } it('should not let the EAP pattern reclassify a Gemini 1.x model', () => { // The EAP character class excludes '.', so a 1.x name carrying the // suffix cannot match and stays below the 2.0 bar. - expect(isGemini2OrAbove('gemini-1.5-flash-early-exp')).toBe(false); + expect(isGeminiEapOr2OrAbove('gemini-1.5-flash-early-exp')).toBe(false); }); + + const eapPathForms = [ + 'models/gemini-flash-early-exp', + 'apigee/gemini-flash-early-exp', + 'gemini/gemini-flash-early-exp', + ]; + + for (const model of eapPathForms) { + it(`should return true for EAP model in path form: ${model}`, () => { + expect(isGeminiEapOr2OrAbove(model)).toBe(true); + }); + } + }); + + describe('numeric versions', () => { + const validModels = [ + 'gemini-2', + 'gemini-2-pro', + 'gemini-2.5-flash', + 'gemini-3.0-pro', + 'projects/12345/locations/us-east1/publishers/google/models/gemini-2.5-pro-preview', + 'models/gemini-2.5-pro', + 'apigee/v1/gemini-2.5-flash', + 'gemini/gemini-2.5-flash', + 'openrouter/google/gemini-2.5-pro:online', + ]; + + for (const model of validModels) { + it(`should return true for model: ${model}`, () => { + expect(isGeminiEapOr2OrAbove(model)).toBe(true); + }); + } + + const invalidModels = [ + 'gemini-1.5-flash', + 'gemini-1.0-pro', + 'openrouter/google/gemini-1.5-pro:online', + 'gemini-2.', + 'gemini-0.9-test', + 'gemini-one', + 'claude-3-sonnet', + '', + 'my-gemini-2.5-model', + ]; + + for (const model of invalidModels) { + it(`should return false for model: ${model || ''}`, () => { + expect(isGeminiEapOr2OrAbove(model)).toBe(false); + }); + } }); }); @@ -336,6 +404,57 @@ describe('isGemini2OrAbove with extended model id forms', () => { } }); +describe('classification consistency', () => { + const allModels = [ + 'gemini-1.5-flash', + 'gemini-2.5-flash', + 'gemini-3.0-pro', + 'gemini-flash-early-exp', + 'gemini/gemini-2.5-flash', + 'openrouter/google/gemini-2.5-pro:online', + 'apigee/gemini-2.5-flash', + 'models/gemini-2.5-pro', + 'claude-3-sonnet', + 'gpt-4', + ]; + + it('should never classify a model as both Gemini 1.x and Gemini EAP/2+', () => { + expect( + allModels.filter( + (model) => isGemini1Model(model) && isGeminiEapOr2OrAbove(model), + ), + ).toEqual([]); + }); + + it('should classify every version-matched model as a Gemini model', () => { + const versionMatched = allModels.filter( + (model) => isGemini1Model(model) || isGeminiEapOr2OrAbove(model), + ); + + expect(versionMatched.length).toBeGreaterThan(0); + expect(versionMatched.filter((model) => !isGeminiModel(model))).toEqual([]); + }); + + const bareNames = [ + 'gemini-1.5-flash', + 'gemini-2.5-flash', + 'gemini-3.0-pro', + 'claude-3-sonnet', + ]; + + for (const bareName of bareNames) { + it(`should classify the bare and path forms of ${bareName} identically`, () => { + const pathForm = `projects/12345/locations/us-central1/publishers/google/models/${bareName}`; + + expect(isGeminiModel(pathForm)).toBe(isGeminiModel(bareName)); + expect(isGemini1Model(pathForm)).toBe(isGemini1Model(bareName)); + expect(isGeminiEapOr2OrAbove(pathForm)).toBe( + isGeminiEapOr2OrAbove(bareName), + ); + }); + } +}); + describe('isGemini3xFlashLive', () => { it('should return true for valid Gemini 3.x Flash Live models', () => { expect(isGemini3xFlashLive('gemini-3.1-flash-live')).toBe(true); From 053035336f87ebb1f59eebd87191b202aebf7992 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Mon, 3 Aug 2026 21:58:51 -0700 Subject: [PATCH 3/4] refactor(utils): trim model_name indirection and pin the Gemini-1 narrowing Review follow-ups: - Inline the single-caller isGeminiEapModel into isGeminiEapOr2OrAbove and drop its docblock, which restated EAP_MODEL_NAME_PATTERN's own. Mirroring adk-python's _is_gemini_eap_model is structural parity, not behavioural. - Drop the duplicated "EAP ids carry no numeric version" sentence from isGeminiEapOr2OrAbove; the constant's docblock and the isGemini2OrAbove pointer already carry it. - Drop the "no `g` flag" clause from both pattern docblocks: it explains the absence of a flag nobody wrote. - Inline MODELS_PREFIX, used twice on adjacent lines while the two path regexes beside it are not similarly hoisted. - Pin the isGemini1Model narrowing at a consumer outside this module. Tightening the predicate to /^gemini-1\.\d+/ also loosens the Gemini 1.x branch of GoogleSearchTool for the undotted ids: `gemini-1` and `gemini-1-pro` now take the googleSearch branch instead of googleSearchRetrieval, and no longer throw when other tools are present. Every existing test there uses a dotted gemini-1.5-* id, so nothing pinned it. Type makeRequest's tools parameter so the new cases typecheck, which also clears a pre-existing TS2322 on the same helper. --- core/src/utils/model_name.ts | 31 +++++-------------- core/test/tools/google_search_tool_test.ts | 35 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/core/src/utils/model_name.ts b/core/src/utils/model_name.ts index 8dfb934a1..6a99e9ad0 100644 --- a/core/src/utils/model_name.ts +++ b/core/src/utils/model_name.ts @@ -8,19 +8,16 @@ import {getBooleanEnvVar} from './env_aware_utils.js'; /** * Path-based model name patterns, tried in order: the Vertex AI publisher path - * and the Apigee path (`apigee/[/][/]`). Declared - * without the `g` flag so `.match()` stays stateless. + * and the Apigee path (`apigee/[/][/]`). */ const MODEL_PATH_PATTERNS = [ /^projects\/[^/]+\/locations\/[^/]+\/publishers\/[^/]+\/models\/(.+)$/, /^apigee\/(?:[^/]+\/)?(?:[^/]+\/)?(.+)$/, ]; -const MODELS_PREFIX = 'models/'; - /** * Matches the Early Access Program (EAP) Gemini naming convention. Lower-case - * only, and without the `g` flag so `.test()` stays stateless. + * only. */ const EAP_MODEL_NAME_PATTERN = /^gemini-[a-z0-9_]+(?:-[a-z0-9_]+)*-early-exp\d*$/; @@ -58,8 +55,8 @@ export function extractModelName(modelString: string): string { } } - if (modelString.startsWith(MODELS_PREFIX)) { - return modelString.slice(MODELS_PREFIX.length); + if (modelString.startsWith('models/')) { + return modelString.slice('models/'.length); } // A 'projects/' string reaching here is a malformed Vertex path. Return it @@ -148,30 +145,18 @@ export function isGemini2OrAbove(modelString: string): boolean { return parsedVersion.valid && parsedVersion.major >= 2; } -/** - * Check if the model is an Early Access Program (EAP) Gemini model, i.e. - * `gemini--early-exp` with an optional numeric suffix — for example - * `gemini-flash-early-exp` or `gemini-flash-lite-early-exp3`. - * - * @param modelString Either a simple model name or path - based model name - * @return true if it's a Gemini EAP model, false otherwise. - */ -function isGeminiEapModel(modelString: string): boolean { - return EAP_MODEL_NAME_PATTERN.test(extractModelName(modelString)); -} - /** * Check if the model is a Gemini EAP or a Gemini 2.0+ model. * - * EAP Gemini models do not encode a numeric version, so they are matched by - * naming convention before any version parsing. - * * @param modelString Either a simple model name or path - based model name * @return true if it's a Gemini EAP model or a Gemini 2.0+ model, false * otherwise. */ export function isGeminiEapOr2OrAbove(modelString: string): boolean { - return isGeminiEapModel(modelString) || isGemini2OrAbove(modelString); + return ( + EAP_MODEL_NAME_PATTERN.test(extractModelName(modelString)) || + isGemini2OrAbove(modelString) + ); } /** diff --git a/core/test/tools/google_search_tool_test.ts b/core/test/tools/google_search_tool_test.ts index f304d94ba..cb8f43e8e 100644 --- a/core/test/tools/google_search_tool_test.ts +++ b/core/test/tools/google_search_tool_test.ts @@ -5,9 +5,13 @@ */ import {GOOGLE_SEARCH, GoogleSearchTool, LlmRequest} from '@google/adk'; +import {GenerateContentConfig} from '@google/genai'; import {describe, expect, it} from 'vitest'; -function makeRequest(model?: string, tools = []): LlmRequest { +function makeRequest( + model?: string, + tools: GenerateContentConfig['tools'] = [], +): LlmRequest { return { model, config: {tools}, @@ -65,6 +69,35 @@ describe('GoogleSearchTool', () => { expect(req.config!.tools).toEqual([{googleSearch: {}}]); }); + const undottedGemini1Ids = ['gemini-1', 'gemini-1-pro', 'gemini-10.0-pro']; + + for (const model of undottedGemini1Ids) { + it(`adds googleSearch, not googleSearchRetrieval, for model: ${model}`, async () => { + const tool = new GoogleSearchTool(); + const req = makeRequest(model); + await tool.processLlmRequest({ + llmRequest: req, + toolContext: {} as never, + }); + + expect(req.config!.tools).toEqual([{googleSearch: {}}]); + }); + + it(`does not reject other tools alongside model: ${model}`, async () => { + const tool = new GoogleSearchTool(); + const req = makeRequest(model, [{functionDeclarations: []}]); + await tool.processLlmRequest({ + llmRequest: req, + toolContext: {} as never, + }); + + expect(req.config!.tools).toEqual([ + {functionDeclarations: []}, + {googleSearch: {}}, + ]); + }); + } + it('throws for unsupported (non-Gemini) model', async () => { const tool = new GoogleSearchTool(); const req = makeRequest('gpt-4'); From 915659382e0f09c24d8dac45288f4244f5a04f9b Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Mon, 3 Aug 2026 22:02:43 -0700 Subject: [PATCH 4/4] test(tools): build a real Context for the new google search cases The new undotted-Gemini-1 cases were passing `{} as never` for toolContext, matching the file's older tests. That is an unchecked cast standing in for a type the tool's signature genuinely requires, so construct a real Context the way url_context_tool_test.ts does. The pre-existing cases keep their own convention rather than being rewritten here. --- core/test/tools/google_search_tool_test.ts | 30 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/core/test/tools/google_search_tool_test.ts b/core/test/tools/google_search_tool_test.ts index cb8f43e8e..dd2ab865b 100644 --- a/core/test/tools/google_search_tool_test.ts +++ b/core/test/tools/google_search_tool_test.ts @@ -4,7 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {GOOGLE_SEARCH, GoogleSearchTool, LlmRequest} from '@google/adk'; +import { + Context, + createSession, + GOOGLE_SEARCH, + GoogleSearchTool, + InvocationContext, + LlmAgent, + LlmRequest, + PluginManager, +} from '@google/adk'; import {GenerateContentConfig} from '@google/genai'; import {describe, expect, it} from 'vitest'; @@ -21,6 +30,21 @@ function makeRequest( } as unknown as LlmRequest; } +function makeToolContext(): Context { + return new Context({ + invocationContext: new InvocationContext({ + invocationId: 'google-search-test', + agent: new LlmAgent({name: 'google_search_test_agent'}), + session: createSession({ + id: 'test-session', + appName: 'test-app', + userId: 'test-user', + }), + pluginManager: new PluginManager([]), + }), + }); +} + describe('GoogleSearchTool', () => { describe('processLlmRequest', () => { it('returns early when model is not set', async () => { @@ -77,7 +101,7 @@ describe('GoogleSearchTool', () => { const req = makeRequest(model); await tool.processLlmRequest({ llmRequest: req, - toolContext: {} as never, + toolContext: makeToolContext(), }); expect(req.config!.tools).toEqual([{googleSearch: {}}]); @@ -88,7 +112,7 @@ describe('GoogleSearchTool', () => { const req = makeRequest(model, [{functionDeclarations: []}]); await tool.processLlmRequest({ llmRequest: req, - toolContext: {} as never, + toolContext: makeToolContext(), }); expect(req.config!.tools).toEqual([