import type { ChatMessage, ResponseProvider } from './runtime'; import { GoogleGenAI, Modality, type LiveServerMessage } from '@google/genai'; type GeminiLiveOptions = { apiKey?: string; model?: string; systemPrompt?: string; }; export class GeminiLiveProvider implements ResponseProvider { private readonly apiKey?: string; private readonly model: string; private readonly systemPrompt: string; private client?: GoogleGenAI; constructor(options: GeminiLiveOptions = {}) { this.apiKey = options.apiKey ?? process.env.GEMINI_API_KEY; this.model = options.model ?? 'gemini-2.0-flash-live-001'; this.systemPrompt = options.systemPrompt ?? 'You are a concise, friendly streaming VTuber agent replying to live chat.'; } async generateReply(message: ChatMessage): Promise { if (!this.apiKey) { throw new Error('GEMINI_API_KEY is not set.'); } this.client ??= new GoogleGenAI({ apiKey: this.apiKey }); const textChunks: string[] = []; const queue: LiveServerMessage[] = []; const session = await this.client.live.connect({ model: this.model.startsWith('models/') ? this.model : `models/${this.model}`, config: { responseModalities: [Modality.TEXT], systemInstruction: this.systemPrompt, }, callbacks: { onmessage: (msg) => { queue.push(msg); const anyMsg = msg as any; const serverContent = anyMsg.serverContent; const directText = anyMsg.text; if (typeof directText === 'string' && directText.trim()) { textChunks.push(directText); } const modelTurn = serverContent?.modelTurn; const parts = modelTurn?.parts ?? []; for (const part of parts) { if (typeof part?.text === 'string' && part.text.trim()) { textChunks.push(part.text); } } const outputTranscription = serverContent?.outputTranscription?.text; if (typeof outputTranscription === 'string' && outputTranscription.trim()) { textChunks.push(outputTranscription); } }, onerror: (e) => { throw e; }, }, }); try { await session.sendClientContent({ turns: `${message.author}: ${message.text}`, } as any); const started = Date.now(); while (Date.now() - started < 15000) { const done = queue.find((msg: any) => msg.serverContent?.turnComplete); if (done) break; await new Promise((resolve) => setTimeout(resolve, 50)); } const reply = textChunks.join(' ').replace(/\s+/g, ' ').trim(); if (!reply) { return `[gemini-live empty:${this.model}] ${message.author}: ${message.text}`; } return reply; } finally { session.close(); } } describeSession() { return { model: this.model, systemPrompt: this.systemPrompt, transport: 'to-be-implemented' }; } }