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
5 changes: 5 additions & 0 deletions .changeset/gemini-provider-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-google': minor
---

Add Gemini provider tools for Google Search, Google Maps, URL context, File Search, code execution, and Vertex RAG retrieval, and serialize them from `ToolContext` for Google LLM and realtime sessions.
43 changes: 11 additions & 32 deletions plugins/google/src/beta/realtime/realtime_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { Mutex } from '@livekit/mutex';
import { AudioFrame, AudioResampler, type VideoFrame } from '@livekit/rtc-node';
import { type LLMTools } from '../../tools.js';
import { toFunctionDeclarations } from '../../utils.js';
import { toToolsConfig } from '../../utils.js';
import type * as api_proto from './api_proto.js';
import type { LiveAPIModels, Voice } from './api_proto.js';

Expand Down Expand Up @@ -70,13 +70,6 @@ export interface InputTranscription {
transcript: string;
}

/**
* Helper function to check if two sets are equal
*/
function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
return a.size === b.size && [...a].every((x) => b.has(x));
}

/**
* Internal realtime options for Google Realtime API
*/
Expand Down Expand Up @@ -455,7 +448,6 @@ export class RealtimeSession extends llm.RealtimeSession {
private _chatCtx = llm.ChatContext.empty();

private options: RealtimeOptions;
private geminiDeclarations: types.FunctionDeclaration[] = [];
private messageChannel = new Queue<api_proto.ClientEvents>();
private inputResampler?: AudioResampler;
private inputResamplerInputRate?: number;
Expand Down Expand Up @@ -764,15 +756,12 @@ export class RealtimeSession extends llm.RealtimeSession {
}

async updateTools(tools: llm.ToolContext): Promise<void> {
const newDeclarations = toFunctionDeclarations(tools);
const currentToolNames = new Set(this.geminiDeclarations.map((f) => f.name));
const newToolNames = new Set(newDeclarations.map((f) => f.name));

if (!setsEqual(currentToolNames, newToolNames)) {
this.geminiDeclarations = newDeclarations;
this._tools = tools;
this.markRestartNeeded();
if (this._tools.equals(tools)) {
return;
}

this._tools = tools;
this.markRestartNeeded();
}

get chatCtx(): llm.ChatContext {
Expand Down Expand Up @@ -1424,21 +1413,11 @@ export class RealtimeSession extends llm.RealtimeSession {
},
languageCode: opts.language,
},
tools:
this.geminiDeclarations.length > 0 || this.options.geminiTools
? [
{
functionDeclarations:
this.options.toolBehavior !== undefined
? this.geminiDeclarations.map((d) => ({
...d,
behavior: this.options.toolBehavior,
}))
: this.geminiDeclarations,
...this.options.geminiTools,
},
]
: undefined,
tools: toToolsConfig({
toolCtx: this._tools,
geminiTools: this.options.geminiTools,
toolBehavior: this.options.toolBehavior,
}),
inputAudioTranscription: opts.inputAudioTranscription,
outputAudioTranscription: opts.outputAudioTranscription,
sessionResumption: this.sessionResumptionHandle
Expand Down
1 change: 1 addition & 0 deletions plugins/google/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Plugin } from '@livekit/agents';
export * as beta from './beta/index.js';
export { LLM, LLMStream, type LLMOptions } from './llm.js';
export * from './models.js';
export * from './tools.js';

class GooglePlugin extends Plugin {
constructor() {
Expand Down
12 changes: 6 additions & 6 deletions plugins/google/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@livekit/agents';
import type { ChatModels } from './models.js';
import type { LLMTools } from './tools.js';
import { toFunctionDeclarations } from './utils.js';
import { toToolsConfig } from './utils.js';

interface GoogleFormatData {
systemMessages: string[] | null;
Expand Down Expand Up @@ -355,11 +355,11 @@ export class LLMStream extends llm.LLMStream {
parts: turn.parts as types.Part[],
}));

const functionDeclarations = this.toolCtx ? toFunctionDeclarations(this.toolCtx) : undefined;
const tools =
functionDeclarations && functionDeclarations.length > 0
? [{ functionDeclarations }]
: undefined;
const tools = toToolsConfig({
toolCtx: this.toolCtx,
geminiTools: this.#geminiTools,
onlySingleType: true,
});

let systemInstruction: types.Content | undefined = undefined;
if (extraData.systemMessages && extraData.systemMessages.length > 0) {
Expand Down
98 changes: 96 additions & 2 deletions plugins/google/src/tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,100 @@
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import type { Tool } from '@google/genai';
import type * as types from '@google/genai';
import { llm } from '@livekit/agents';

export type LLMTools = Omit<Tool, 'functionDeclarations'>;
export type LLMTools = Omit<types.Tool, 'functionDeclarations'>;

export abstract class GeminiTool extends llm.ProviderTool {
abstract toToolConfig(): types.Tool;
}

export class GoogleSearch extends GeminiTool {
constructor(public readonly options: types.GoogleSearch = {}) {
super({ id: 'gemini_google_search' });
}

toToolConfig(): types.Tool {
return { googleSearch: this.options };
}
}

export class GoogleMaps extends GeminiTool {
constructor(public readonly options: types.GoogleMaps = {}) {
super({ id: 'gemini_google_maps' });
}

toToolConfig(): types.Tool {
return { googleMaps: this.options };
}
}

export class URLContext extends GeminiTool {
constructor() {
super({ id: 'gemini_url_context' });
}

toToolConfig(): types.Tool {
return { urlContext: {} };
}
}

export interface FileSearchOptions extends types.FileSearch {
fileSearchStoreNames: string[];
}

export class FileSearch extends GeminiTool {
constructor(public readonly options: FileSearchOptions) {
super({ id: 'gemini_file_search' });
}

toToolConfig(): types.Tool {
return { fileSearch: this.options };
}
}

export class ToolCodeExecution extends GeminiTool {
constructor() {
super({ id: 'gemini_code_execution' });
}

toToolConfig(): types.Tool {
return { codeExecution: {} };
}
}

export interface VertexRAGRetrievalOptions {
ragResources: string[];
similarityTopK?: number;
vectorDistanceThreshold?: number;
}

export class VertexRAGRetrieval extends GeminiTool {
readonly ragResources: string[];
readonly similarityTopK: number;
readonly vectorDistanceThreshold?: number;

constructor({
ragResources,
similarityTopK = 3,
vectorDistanceThreshold,
}: VertexRAGRetrievalOptions) {
super({ id: 'gemini_vertex_rag_retrieval' });
this.ragResources = ragResources;
this.similarityTopK = similarityTopK;
this.vectorDistanceThreshold = vectorDistanceThreshold;
}

toToolConfig(): types.Tool {
return {
retrieval: {
vertexRagStore: {
ragResources: this.ragResources.map((ragCorpus) => ({ ragCorpus })),
similarityTopK: this.similarityTopK,
vectorDistanceThreshold: this.vectorDistanceThreshold,
},
},
};
}
}
56 changes: 56 additions & 0 deletions plugins/google/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import type * as types from '@google/genai';
import type { FunctionDeclaration, Schema } from '@google/genai';
import { llm } from '@livekit/agents';
import type { JSONSchema7 } from 'json-schema';
import { GeminiTool, type LLMTools } from './tools.js';

/**
* JSON Schema v7
Expand Down Expand Up @@ -157,3 +159,57 @@ export function toFunctionDeclarations(toolCtx: llm.ToolContext): FunctionDeclar

return functionDeclarations;
}

export function toToolsConfig({
toolCtx,
geminiTools,
toolBehavior,
onlySingleType = false,
}: {
toolCtx?: llm.ToolContext;
geminiTools?: LLMTools;
toolBehavior?: types.Behavior;
onlySingleType?: boolean;
}): types.Tool[] | undefined {
const tools: types.Tool[] = [];
const providerTools: types.Tool[] = [];

if (toolCtx) {
const functionDeclarations = toFunctionDeclarations(toolCtx);
if (functionDeclarations.length > 0) {
tools.push({
functionDeclarations:
toolBehavior !== undefined
? functionDeclarations.map((declaration) => ({
...declaration,
behavior: toolBehavior,
}))
: functionDeclarations,
});
}
}

if (geminiTools !== undefined) {
providerTools.push(geminiTools);
}

if (toolCtx) {
for (const tool of toolCtx.providerTools) {
if (tool instanceof GeminiTool) {
providerTools.push(tool.toToolConfig());
}
}
}

if (tools.length > 0 && providerTools.length > 0) {
throw new Error('Gemini does not support mixing function tools and provider tools');
}
Comment on lines +192 to +206
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Regression: geminiTools combined with function tools now throws instead of merging into a single Tool object

The old code in buildConnectConfig merged geminiTools (e.g., {googleSearch: {}}) and functionDeclarations into a single Tool object via the spread operator: [{ functionDeclarations: [...], ...this.options.geminiTools }]. The new toToolsConfig function treats geminiTools as a separate "provider tool" (plugins/google/src/utils.ts:192-193) and then throws an error at plugins/google/src/utils.ts:204-206 if both function declarations and provider tools are present. This breaks any existing user who configures both geminiTools (e.g., {googleSearch: {}}) and function tools in a realtime session — a combination that worked before and is valid in the Google API (a single Tool object can contain both functionDeclarations and other tool types like googleSearch). The fix should merge geminiTools into the function declarations Tool object (matching the old behavior) rather than pushing it into the separate providerTools array.

Prompt for agents
The toToolsConfig function in plugins/google/src/utils.ts incorrectly treats the geminiTools option as a separate provider tool, causing a runtime error when users combine geminiTools (e.g. {googleSearch: {}}) with function tools. The old behavior in plugins/google/src/beta/realtime/realtime_api.ts merged geminiTools into the same Tool object as functionDeclarations using spread: [{functionDeclarations: [...], ...geminiTools}].

The fix: instead of pushing geminiTools into the providerTools array (line 192-194), merge it into the function declarations Tool object when function declarations exist, or push it as a standalone Tool when no function declarations are present. This maintains backward compatibility while still throwing when new GeminiTool provider tool instances (from toolCtx.providerTools) are mixed with function tools.

Approach: When building the function declarations Tool object (lines 177-189), spread geminiTools into it (similar to the old realtime code). Remove the geminiTools push from providerTools (line 192-194). If there are no function declarations but geminiTools is defined, push it as a standalone Tool object in the tools array.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


if (onlySingleType && tools.length > 0) {
return tools;
}

tools.push(...providerTools);

return tools.length > 0 ? tools : undefined;
}