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
26 changes: 26 additions & 0 deletions src/common/utils/tools/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RUNTIME_MODE } from "@/common/types/runtime";
import {
buildTaskToolDescription,
getAvailableTools,
supportsGoogleNativeToolsWithFunctionTools,
TaskToolArgsSchema,
TOOL_DEFINITIONS,
} from "./toolDefinitions";
Expand Down Expand Up @@ -424,6 +425,31 @@ describe("TOOL_DEFINITIONS", () => {
expect(enabledTools).toContain("workflow_run");
});

it("gates native Google tools to Gemini 3 models", () => {
expect(getAvailableTools("google:gemini-2.5-pro")).not.toContain("google_search");
expect(getAvailableTools("google:gemini-2.5-pro")).not.toContain("url_context");
expect(getAvailableTools("google:gemini-4-pro")).not.toContain("google_search");
expect(getAvailableTools("google:gemini-4-pro")).not.toContain("url_context");

for (const modelString of [
"google:gemini-3.1-pro-preview",
"google:gemini-3.5-flash",
"google:models/gemini-3.5-flash",
]) {
const tools = getAvailableTools(modelString);
expect(tools).toContain("google_search");
expect(tools).toContain("url_context");
}
});

it("classifies Gemini 3 as supporting mixed native Google and function tools", () => {
expect(supportsGoogleNativeToolsWithFunctionTools("gemini-2.5-pro")).toBe(false);
expect(supportsGoogleNativeToolsWithFunctionTools("gemini-3.1-pro-preview")).toBe(true);
expect(supportsGoogleNativeToolsWithFunctionTools("gemini-3.5-flash")).toBe(true);
expect(supportsGoogleNativeToolsWithFunctionTools("models/gemini-3.5-flash")).toBe(true);
expect(supportsGoogleNativeToolsWithFunctionTools("gemini-4-pro")).toBe(false);
});

it("agent_skill_write schema rejects an advertise tool argument (advertise is authored in content)", () => {
const parsed = TOOL_DEFINITIONS.agent_skill_write.schema.safeParse({
name: "demo-skill",
Expand Down
20 changes: 18 additions & 2 deletions src/common/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,19 @@ export function getToolSchemas(): Record<string, ToolSchema> {
);
}

/**
* Google's mixed built-in + function tool path is currently supported for Gemini 3.
* Keep native Google tools gated here so the prompt allowlist matches the actual toolset.
*/
export function supportsGoogleNativeToolsWithFunctionTools(modelId: string): boolean {
const bareModelId = modelId.split("/").pop() ?? modelId;
const match = /^gemini-(\d+)(?:[.-]|$)/.exec(bareModelId);
if (!match) return false;
const major = Number.parseInt(match[1], 10);
// The installed @ai-sdk/google mixed native/function serialization path is Gemini 3-only.
return major === 3;
}

/**
* Get which tools are available for a given model
* @param modelString The model string (e.g., "anthropic:claude-opus-4-1")
Expand All @@ -2348,7 +2361,7 @@ export function getAvailableTools(
enableMuxGlobalAgentsTools?: boolean;
}
): string[] {
const [provider] = modelString.split(":");
const [provider, modelId = ""] = modelString.split(":");
const enableAgentReport = options?.enableAgentReport ?? true;
const enableAnalyticsQuery = options?.enableAnalyticsQuery ?? true;
const enableAdvisor = options?.enableAdvisor ?? false;
Expand Down Expand Up @@ -2416,7 +2429,10 @@ export function getAvailableTools(
}
return baseTools;
case "google":
return [...baseTools, "google_search"];
if (supportsGoogleNativeToolsWithFunctionTools(modelId)) {
return [...baseTools, "google_search", "url_context"];
}
return baseTools;
default:
return baseTools;
}
Expand Down
61 changes: 61 additions & 0 deletions src/common/utils/tools/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,67 @@ describe("getToolsForModel", () => {
expect(Object.keys(tools).filter((toolName) => toolName.startsWith("desktop_"))).toEqual([]);
});

test("adds native Google Search and URL Context only for Gemini 3 models", async () => {
const runtime = new LocalRuntime(process.cwd());
const initStateManager = createInitStateManager();

const gemini25Tools = await getToolsForModel(
"google:gemini-2.5-pro",
{
cwd: process.cwd(),
runtime,
runtimeTempDir: "/tmp",
workspaceId: "ws-1",
},
"ws-1",
initStateManager
);
expect(gemini25Tools.google_search).toBeUndefined();
expect(gemini25Tools.url_context).toBeUndefined();

const gemini4Tools = await getToolsForModel(
"google:gemini-4-pro",
{
cwd: process.cwd(),
runtime,
runtimeTempDir: "/tmp",
workspaceId: "ws-1",
},
"ws-1",
initStateManager
);
expect(gemini4Tools.google_search).toBeUndefined();
expect(gemini4Tools.url_context).toBeUndefined();

const gemini35Tools = await getToolsForModel(
"google:gemini-3.5-flash",
{
cwd: process.cwd(),
runtime,
runtimeTempDir: "/tmp",
workspaceId: "ws-1",
},
"ws-1",
initStateManager
);
const namespacedGemini35Tools = await getToolsForModel(
"google:models/gemini-3.5-flash",
{
cwd: process.cwd(),
runtime,
runtimeTempDir: "/tmp",
workspaceId: "ws-1",
},
"ws-1",
initStateManager
);
expect(namespacedGemini35Tools.google_search).toBeDefined();
expect(namespacedGemini35Tools.url_context).toBeDefined();

expect(gemini35Tools.google_search).toBeDefined();
expect(gemini35Tools.url_context).toBeDefined();
});

test("returns tool keys in sorted order", async () => {
const runtime = new LocalRuntime(process.cwd());
const initStateManager = createInitStateManager();
Expand Down
23 changes: 18 additions & 5 deletions src/common/utils/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ import { log } from "@/node/services/log";
import { attachModelOnlyToolNotifications } from "@/common/utils/tools/internalToolResultFields";
import { NotificationEngine } from "@/node/services/agentNotifications/NotificationEngine";
import { TodoListReminderSource } from "@/node/services/agentNotifications/sources/TodoListReminderSource";
import { getAvailableTools } from "@/common/utils/tools/toolDefinitions";
import {
getAvailableTools,
supportsGoogleNativeToolsWithFunctionTools,
} from "@/common/utils/tools/toolDefinitions";
import { sanitizeMCPToolsForOpenAI } from "@/common/utils/tools/schemaSanitizer";

import type { Runtime } from "@/node/runtime/Runtime";
Expand Down Expand Up @@ -594,10 +597,20 @@ export async function getToolsForModel(
break;
}

// Note: Gemini 3 tool support:
// Combining native tools with function calling is currently only
// supported in the Live API. Thus no `google_search` or `url_context` added here.
// - https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#native-tools
case "google": {
if (supportsGoogleNativeToolsWithFunctionTools(modelId)) {
const { google } = await import("@ai-sdk/google");
allTools = {
...baseTools,
...(mcpTools ?? {}),
// Google exposes native Search and URL Context as provider-executed tools for
// Gemini 3+. These coexist with Mux function tools in the standard streaming API.
google_search: google.tools.googleSearch({}) as Tool,
url_context: google.tools.urlContext({}) as Tool,
};
}
break;
}
}
} catch (error) {
// If tools aren't available, just use base tools
Expand Down
12 changes: 12 additions & 0 deletions src/node/utils/main/tokenizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,16 @@ describe("tokenizer", () => {
);
expect(tokens).toBeGreaterThan(0);
});

test("uses native Google tool token fallbacks for Gemini 3", async () => {
await expect(
getToolDefinitionTokens("url_context", "google:gemini-2.5-pro", undefined, undefined)
).resolves.toBe(0);
await expect(
getToolDefinitionTokens("url_context", "google:gemini-3.5-flash", undefined, undefined)
).resolves.toBe(50);
await expect(
getToolDefinitionTokens("google_search", "google:gemini-3.5-flash", undefined, undefined)
).resolves.toBe(50);
});
});
24 changes: 13 additions & 11 deletions src/node/utils/main/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ export function countTokensForData(data: unknown, tokenizer: Tokenizer): Promise
return tokenizer.countTokens(serialized);
}

const TOOL_DEFINITION_FALLBACK_TOKENS: Record<string, number> = {
bash: 65,
file_read: 45,
file_edit_replace_string: 70,
file_edit_replace_lines: 80,
file_edit_insert: 50,
web_search: 50,
google_search: 50,
url_context: 50,
};

export async function getToolDefinitionTokens(
toolName: string,
modelString: string,
Expand All @@ -260,22 +271,13 @@ export async function getToolDefinitionTokens(
const toolSchemas = getToolSchemas();
const toolSchema = toolSchemas[toolName];
if (!toolSchema) {
return 40;
return TOOL_DEFINITION_FALLBACK_TOKENS[toolName] ?? 40;
}

const tokenizerModel = metadataModelOverride ?? modelString;
return countTokens(tokenizerModel, JSON.stringify(toolSchema));
} catch {
const fallbackSizes: Record<string, number> = {
bash: 65,
file_read: 45,
file_edit_replace_string: 70,
file_edit_replace_lines: 80,
file_edit_insert: 50,
web_search: 50,
google_search: 50,
};
return fallbackSizes[toolName] ?? 40;
return TOOL_DEFINITION_FALLBACK_TOKENS[toolName] ?? 40;
}
}

Expand Down
Loading