Skip to content
Open
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
6 changes: 5 additions & 1 deletion core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,11 @@ export {VertexRagRetrievalTool} from './tools/vertex_rag_retrieval_tool.js';
export {getClientLabels, runWithClientLabel} from './utils/client_labels.js';
export {LogLevel, getLogger, setLogLevel, setLogger} from './utils/logger.js';
export type {Logger} from './utils/logger.js';
export {isGemini2OrAbove, isGemini3xFlashLive} from './utils/model_name.js';
export {
isGemini2OrAbove,
isGemini35LiveTranslate,
isGemini3xLive,
} from './utils/model_name.js';
export {zodObjectToSchema} from './utils/simple_zod_to_json.js';
export {Task} from './utils/task.js';
export type {TaskExecutable} from './utils/task.js';
Expand Down
8 changes: 4 additions & 4 deletions core/src/models/gemini_llm_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {

import {LiveResponseAggregator} from '../utils/live_connection_utils.js';
import {logger} from '../utils/logger.js';
import {isGemini3xFlashLive} from '../utils/model_name.js';
import {isGemini3xLive} from '../utils/model_name.js';

import {BaseLlmConnection} from './base_llm_connection.js';
import {LlmResponse} from './llm_response.js';
Expand Down Expand Up @@ -43,7 +43,7 @@ export class GeminiLlmConnection implements BaseLlmConnection {
);

if (contents.length > 0) {
const isGemini3x = isGemini3xFlashLive(this.modelVersion);
const isGemini3x = isGemini3xLive(this.modelVersion);
this.geminiSession.sendClientContent({
turns: contents,
turnComplete: isGemini3x
Expand Down Expand Up @@ -79,7 +79,7 @@ export class GeminiLlmConnection implements BaseLlmConnection {
});
} else {
logger.debug('Sending LLM new content', content);
const isGemini3x = isGemini3xFlashLive(this.modelVersion);
const isGemini3x = isGemini3xLive(this.modelVersion);
if (isGemini3x && content.parts.length === 1 && content.parts[0].text) {
logger.debug('Using sendRealtimeInput for Gemini 3.x text input');
this.geminiSession.sendRealtimeInput({text: content.parts[0].text});
Expand All @@ -99,7 +99,7 @@ export class GeminiLlmConnection implements BaseLlmConnection {
*/
async sendRealtime(blob: Blob): Promise<void> {
logger.debug('Sending LLM Blob:', blob);
const isGemini3x = isGemini3xFlashLive(this.modelVersion);
const isGemini3x = isGemini3xLive(this.modelVersion);
const isNativeAudio = this.modelVersion?.includes('native-audio');

if (isGemini3x || isNativeAudio) {
Expand Down
4 changes: 2 additions & 2 deletions core/src/utils/live_connection_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Part,
} from '@google/genai';
import {LlmResponse} from '../models/llm_response.js';
import {isGemini3xFlashLive} from './model_name.js';
import {isGemini3xLive} from './model_name.js';

/**
* Aggregator and mapper for Gemini Live WebSocket server messages.
Expand Down Expand Up @@ -243,7 +243,7 @@ export class LiveResponseAggregator {
);
}

const isGemini3x = isGemini3xFlashLive(this.modelVersion);
const isGemini3x = isGemini3xLive(this.modelVersion);
if (isGemini3x && this.toolCallParts.length > 0) {
yield {
content: {role: 'model', parts: this.toolCallParts},
Expand Down
30 changes: 26 additions & 4 deletions core/src/utils/model_name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,39 @@ export function isGemini2OrAbove(modelString: string): boolean {
}

/**
* Check if the model is a Gemini 3.x Flash Live model.
* Check if the model is a Gemini 3.x Live model.
*
* Live Translate models are excluded; use {@link isGemini35LiveTranslate} for
* those.
*
* @param modelString Either a simple model name or path-based model name
* @return true if it's a Gemini 3.x Flash Live model, false otherwise.
* @return true if it's a Gemini 3.x Live model, false otherwise.
*/
export function isGemini3xFlashLive(modelString: string | undefined): boolean {
export function isGemini3xLive(modelString: string | undefined): boolean {
if (!modelString) {
return false;
}
const modelName = extractModelName(modelString);
return modelName.startsWith('gemini-3.') && modelName.includes('-flash-live');
return (
modelName.startsWith('gemini-3.') &&
modelName.includes('-live') &&
!isGemini35LiveTranslate(modelString)
);
}

/**
* Check if the model is a Gemini 3.5 Live Translate model.
*
* @param modelString Either a simple model name or path-based model name
* @return true if it's a Gemini 3.5 Live Translate model, false otherwise.
*/
export function isGemini35LiveTranslate(
modelString: string | undefined,
): boolean {
if (!modelString) {
return false;
}
return extractModelName(modelString).startsWith('gemini-3.5-live-translate');
}

/**
Expand Down
65 changes: 64 additions & 1 deletion core/test/models/gemini_llm_connection_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ describe('GeminiLlmConnection', () => {
});
});

it('should send history with turnComplete=true for non-flash Gemini 3.x Live', async () => {
const connection = new GeminiLlmConnection(
mockSession,
'gemini-3.5-flash-lite-live-preview',
);
const history: Content[] = [
{role: 'user', parts: [{text: 'hello'}]},
{role: 'model', parts: [{text: 'hi'}]},
];

await connection.sendHistory(history);

expect(mockSession.sendClientContent).toHaveBeenCalledWith({
turns: history,
turnComplete: true,
});
});

it('should not send history if empty', async () => {
const connection = new GeminiLlmConnection(
mockSession,
Expand Down Expand Up @@ -118,6 +136,23 @@ describe('GeminiLlmConnection', () => {
});
});

it('should use sendRealtimeInput for non-flash Gemini 3.x Live single-part text', async () => {
const connection = new GeminiLlmConnection(
mockSession,
'gemini-3.5-flash-lite-live-preview',
);
const content: Content = {
parts: [{text: 'hello'}],
};

await connection.sendContent(content);

expect(mockSession.sendRealtimeInput).toHaveBeenCalledWith({
text: 'hello',
});
expect(mockSession.sendClientContent).not.toHaveBeenCalled();
});

it('should use sendClientContent for non-Gemini 3.x single-part text', async () => {
const connection = new GeminiLlmConnection(
mockSession,
Expand Down Expand Up @@ -189,6 +224,34 @@ describe('GeminiLlmConnection', () => {
});
});

it('should use sendRealtimeInput with audio for non-flash Gemini 3.x Live audio', async () => {
const connection = new GeminiLlmConnection(
mockSession,
'gemini-3.5-flash-lite-live-preview',
);
const blob: Blob = {mimeType: 'audio/pcm', data: 'base64data'};

await connection.sendRealtime(blob);

expect(mockSession.sendRealtimeInput).toHaveBeenCalledWith({
audio: blob,
});
});

it('should use sendRealtimeInput with media for Live Translate audio', async () => {
const connection = new GeminiLlmConnection(
mockSession,
'gemini-3.5-live-translate',
);
const blob: Blob = {mimeType: 'audio/pcm', data: 'base64data'};

await connection.sendRealtime(blob);

expect(mockSession.sendRealtimeInput).toHaveBeenCalledWith({
media: blob,
});
});

it('should use sendRealtimeInput with audio for Native Audio model audio', async () => {
const connection = new GeminiLlmConnection(
mockSession,
Expand Down Expand Up @@ -790,7 +853,7 @@ describe('GeminiLlmConnection', () => {
expect((await generator.next()).done).toBe(true);
});

it('should handle undefined modelVersion in isGemini3xFlashLive check', async () => {
it('should handle undefined modelVersion in isGemini3xLive check', async () => {
const connection = new GeminiLlmConnection(
mockSession,
undefined,
Expand Down
23 changes: 23 additions & 0 deletions core/test/utils/live_connection_utils_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,29 @@ describe('LiveResponseAggregator', () => {
]);
});

it('should yield tool calls immediately for non-flash Gemini 3.x Live', () => {
const aggregator = new LiveResponseAggregator(
'gemini-3.5-flash-lite-live-preview',
);

const res1 = Array.from(
aggregator.processMessage({
toolCall: {
functionCalls: [{name: 'tool_a', args: {x: 1}, id: '1'}],
},
}),
);
expect(res1).toEqual([
{
content: {
role: 'model',
parts: [{functionCall: {name: 'tool_a', args: {x: 1}, id: '1'}}],
},
modelVersion: 'gemini-3.5-flash-lite-live-preview',
},
]);
});

it('should yield session resumption update', () => {
const aggregator = new LiveResponseAggregator('gemini-2.5-flash');
const resumptionUpdate = {resumed: true};
Expand Down
68 changes: 56 additions & 12 deletions core/test/utils/model_name_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {isGemini2OrAbove, isGemini3xFlashLive} from '@google/adk';
import {
isGemini2OrAbove,
isGemini35LiveTranslate,
isGemini3xLive,
} from '@google/adk';
import {describe, expect, it} from 'vitest';

describe('isGemini2OrAbove', () => {
Expand Down Expand Up @@ -55,28 +59,68 @@ describe('isGemini2OrAbove', () => {
});
});

describe('isGemini3xFlashLive', () => {
describe('isGemini3xLive', () => {
it('should return true for valid Gemini 3.x Flash Live models', () => {
expect(isGemini3xFlashLive('gemini-3.1-flash-live')).toBe(true);
expect(isGemini3xFlashLive('gemini-3.1-flash-live-preview')).toBe(true);
expect(isGemini3xFlashLive('gemini-3.5-flash-live')).toBe(true);
expect(isGemini3xFlashLive('gemini-3.5-flash-live-preview')).toBe(true);
expect(isGemini3xLive('gemini-3.1-flash-live')).toBe(true);
expect(isGemini3xLive('gemini-3.1-flash-live-preview')).toBe(true);
expect(isGemini3xLive('gemini-3.5-flash-live')).toBe(true);
expect(isGemini3xLive('gemini-3.5-flash-live-preview')).toBe(true);
expect(
isGemini3xFlashLive(
isGemini3xLive(
'projects/my-project/locations/us-central1/publishers/google/models/gemini-3.1-flash-live-001',
),
).toBe(true);
expect(
isGemini3xFlashLive(
isGemini3xLive(
'projects/my-project/locations/us-central1/publishers/google/models/gemini-3.5-flash-live-001',
),
).toBe(true);
});

it('should return false for other models', () => {
expect(isGemini3xFlashLive('gemini-2.5-flash')).toBe(false);
expect(isGemini3xFlashLive('gemini-3.0-flash')).toBe(false);
expect(isGemini3xFlashLive(undefined)).toBe(false);
expect(isGemini3xFlashLive('')).toBe(false);
expect(isGemini3xLive('gemini-2.5-flash')).toBe(false);
expect(isGemini3xLive('gemini-3.0-flash')).toBe(false);
expect(isGemini3xLive(undefined)).toBe(false);
expect(isGemini3xLive('')).toBe(false);
});

it('should return true for non-flash Gemini 3.x Live models', () => {
expect(isGemini3xLive('gemini-3.5-flash-lite-live-preview')).toBe(true);
expect(
isGemini3xLive(
'projects/my-project/locations/us-central1/publishers/google/models/gemini-3.5-flash-lite-live-preview',
),
).toBe(true);
});

it('should return false for Live Translate models', () => {
expect(isGemini3xLive('gemini-3.5-live-translate')).toBe(false);
expect(
isGemini3xLive(
'projects/my-project/locations/us-central1/publishers/google/models/gemini-3.5-live-translate-preview',
),
).toBe(false);
});

it('should return false for non-live and pre-3.x live models', () => {
expect(isGemini3xLive('gemini-3.1-pro')).toBe(false);
expect(isGemini3xLive('gemini-2.5-flash-live')).toBe(false);
});
});

describe('isGemini35LiveTranslate', () => {
it('should return true for Live Translate models', () => {
expect(isGemini35LiveTranslate('gemini-3.5-live-translate')).toBe(true);
expect(
isGemini35LiveTranslate(
'projects/my-project/locations/us-central1/publishers/google/models/gemini-3.5-live-translate-preview',
),
).toBe(true);
});

it('should return false for other models', () => {
expect(isGemini35LiveTranslate('gemini-3.5-flash-live')).toBe(false);
expect(isGemini35LiveTranslate(undefined)).toBe(false);
expect(isGemini35LiveTranslate('')).toBe(false);
});
});
Loading