diff --git a/changelog/3980.deprecated.md b/changelog/3980.deprecated.md new file mode 100644 index 000000000..f69964b62 --- /dev/null +++ b/changelog/3980.deprecated.md @@ -0,0 +1 @@ +- Deprecated `pipecat.services.google.llm_vertex`, `pipecat.services.google.llm_openai`, and `pipecat.services.google.gemini_live.llm_vertex` modules. Use `pipecat.services.google.vertex.llm`, `pipecat.services.google.openai.llm`, and `pipecat.services.google.gemini_live.vertex.llm` instead. The old import paths still work but will emit a `DeprecationWarning`. diff --git a/examples/foundational/14o-function-calling-gemini-openai-format.py b/examples/foundational/14o-function-calling-gemini-openai-format.py index 416e602bf..8ab53d9cf 100644 --- a/examples/foundational/14o-function-calling-gemini-openai-format.py +++ b/examples/foundational/14o-function-calling-gemini-openai-format.py @@ -26,7 +26,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.services.elevenlabs.tts import ElevenLabsTTSService -from pipecat.services.google.llm_openai import GoogleLLMOpenAIBetaService +from pipecat.services.google.openai.llm import GoogleLLMOpenAIBetaService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams diff --git a/examples/foundational/14p-function-calling-gemini-vertex-ai.py b/examples/foundational/14p-function-calling-gemini-vertex-ai.py index 1f33efacf..c6259b992 100644 --- a/examples/foundational/14p-function-calling-gemini-vertex-ai.py +++ b/examples/foundational/14p-function-calling-gemini-vertex-ai.py @@ -26,7 +26,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.services.elevenlabs.tts import ElevenLabsTTSService -from pipecat.services.google.llm_vertex import GoogleVertexLLMService +from pipecat.services.google.vertex.llm import GoogleVertexLLMService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams diff --git a/examples/foundational/26h-gemini-live-vertex-function-calling.py b/examples/foundational/26h-gemini-live-vertex-function-calling.py index 18e417f22..7dd894fd1 100644 --- a/examples/foundational/26h-gemini-live-vertex-function-calling.py +++ b/examples/foundational/26h-gemini-live-vertex-function-calling.py @@ -26,7 +26,7 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.google.gemini_live.llm_vertex import GeminiLiveVertexLLMService +from pipecat.services.google.gemini_live.vertex.llm import GeminiLiveVertexLLMService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams diff --git a/examples/foundational/55zk-update-settings-google-vertex-llm.py b/examples/foundational/55zk-update-settings-google-vertex-llm.py index 6b2babb7f..7f7a34cba 100644 --- a/examples/foundational/55zk-update-settings-google-vertex-llm.py +++ b/examples/foundational/55zk-update-settings-google-vertex-llm.py @@ -24,7 +24,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService -from pipecat.services.google.llm_vertex import GoogleVertexLLMService +from pipecat.services.google.vertex.llm import GoogleVertexLLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams diff --git a/examples/foundational/55zm-update-settings-gemini-live-vertex.py b/examples/foundational/55zm-update-settings-gemini-live-vertex.py index f69b94557..8ff18e1eb 100644 --- a/examples/foundational/55zm-update-settings-gemini-live-vertex.py +++ b/examples/foundational/55zm-update-settings-gemini-live-vertex.py @@ -19,7 +19,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.google.gemini_live.llm_vertex import GeminiLiveVertexLLMService +from pipecat.services.google.gemini_live.vertex.llm import GeminiLiveVertexLLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams diff --git a/src/pipecat/services/google/__init__.py b/src/pipecat/services/google/__init__.py index 032cf0eb8..32b12e367 100644 --- a/src/pipecat/services/google/__init__.py +++ b/src/pipecat/services/google/__init__.py @@ -12,12 +12,12 @@ from .frames import * from .gemini_live import * from .image import * from .llm import * -from .llm_openai import * -from .llm_vertex import * +from .openai import * from .rtvi import * from .stt import * from .tts import * +from .vertex import * sys.modules[__name__] = DeprecatedModuleProxy( - globals(), "google", "google.[frames,image,llm,llm_openai,llm_vertex,rtvi,stt,tts]" + globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]" ) diff --git a/src/pipecat/services/google/gemini_live/__init__.py b/src/pipecat/services/google/gemini_live/__init__.py index f4bfbb5c8..4afeb99ce 100644 --- a/src/pipecat/services/google/gemini_live/__init__.py +++ b/src/pipecat/services/google/gemini_live/__init__.py @@ -1,6 +1,6 @@ from .file_api import GeminiFileAPI from .llm import GeminiLiveLLMService -from .llm_vertex import GeminiLiveVertexLLMService +from .vertex.llm import GeminiLiveVertexLLMService __all__ = [ "GeminiFileAPI", diff --git a/src/pipecat/services/google/gemini_live/llm_vertex.py b/src/pipecat/services/google/gemini_live/llm_vertex.py index cdfde1660..038d72e57 100644 --- a/src/pipecat/services/google/gemini_live/llm_vertex.py +++ b/src/pipecat/services/google/gemini_live/llm_vertex.py @@ -4,277 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Service for accessing Gemini Live via Google Vertex AI. +"""Deprecated: use ``pipecat.services.google.gemini_live.vertex.llm`` instead.""" -This module provides integration with Google's Gemini Live model via -Vertex AI, supporting both text and audio modalities with voice transcription, -streaming responses, and tool usage. -""" +import warnings -import json -from dataclasses import dataclass -from typing import List, Optional, Union - -from loguru import logger - -from pipecat.adapters.schemas.tools_schema import ToolsSchema -from pipecat.services.google.gemini_live.llm import ( - GeminiLiveLLMService, - GeminiLiveLLMSettings, - GeminiMediaResolution, - GeminiModalities, - HttpOptions, - InputParams, - language_to_gemini_language, +warnings.warn( + "Module `pipecat.services.google.gemini_live.llm_vertex` is deprecated, " + "use `pipecat.services.google.gemini_live.vertex.llm` instead.", + DeprecationWarning, + stacklevel=2, ) -from pipecat.services.settings import _warn_deprecated_param -try: - from google.auth import default - from google.auth.exceptions import GoogleAuthError - from google.auth.transport.requests import Request - from google.genai import Client - from google.oauth2 import service_account - -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error("In order to use Google Vertex AI, you need to `pip install pipecat-ai[google]`.") - raise Exception(f"Missing module: {e}") - - -@dataclass -class GeminiLiveVertexLLMSettings(GeminiLiveLLMSettings): - """Settings for GeminiLiveVertexLLMService.""" - - pass - - -class GeminiLiveVertexLLMService(GeminiLiveLLMService): - """Provides access to Google's Gemini Live model via Vertex AI. - - This service enables real-time conversations with Gemini, supporting both - text and audio modalities. It handles voice transcription, streaming audio - responses, and tool usage. - """ - - Settings = GeminiLiveVertexLLMSettings - _settings: GeminiLiveVertexLLMSettings - - def __init__( - self, - *, - credentials: Optional[str] = None, - credentials_path: Optional[str] = None, - location: str, - project_id: str, - model: Optional[str] = None, - voice_id: str = "Charon", - start_audio_paused: bool = False, - start_video_paused: bool = False, - system_instruction: Optional[str] = None, - tools: Optional[Union[List[dict], ToolsSchema]] = None, - params: Optional[InputParams] = None, - settings: Optional[GeminiLiveVertexLLMSettings] = None, - inference_on_context_initialization: bool = True, - file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files", - http_options: Optional[HttpOptions] = None, - **kwargs, - ): - """Initialize the service for accessing Gemini Live via Google Vertex AI. - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - location: GCP region for Vertex AI endpoint (e.g., "us-east4"). - project_id: Google Cloud project ID. - model: Model identifier to use. - - .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(model=...)`` instead. - - voice_id: TTS voice identifier. Defaults to "Charon". - - .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveVertexLLMSettings(voice=...)`` instead. - start_audio_paused: Whether to start with audio input paused. Defaults to False. - start_video_paused: Whether to start with video input paused. Defaults to False. - system_instruction: System prompt for the model. Defaults to None. - tools: Tools/functions available to the model. Defaults to None. - params: Configuration parameters for the model along with Vertex AI - location and project ID. - - .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(...)`` instead. - - settings: Gemini Live LLM settings. If provided together with deprecated - top-level parameters, the ``settings`` values take precedence. - inference_on_context_initialization: Whether to generate a response when context - is first set. Defaults to True. - file_api_base_url: Base URL for the Gemini File API. Defaults to the official endpoint. - http_options: HTTP options for the client. - **kwargs: Additional arguments passed to parent GeminiLiveLLMService. - """ - # Check if user incorrectly passed api_key, which is used by parent - # class but not here. - if "api_key" in kwargs: - logger.error( - "GeminiLiveVertexLLMService does not accept 'api_key' parameter. " - "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." - ) - raise ValueError( - "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." - ) - - # These need to be set before calling super().__init__() because - # super().__init__() invokes create_client(), which needs these. - self._credentials = self._get_credentials(credentials, credentials_path) - self._project_id = project_id - self._location = location - - # Build default_settings from deprecated args, then apply settings delta. - # We pass settings= to super() instead of model=/params= to avoid - # double deprecation warnings from the parent. - - # 1. Initialize default_settings with hardcoded defaults - default_settings = GeminiLiveVertexLLMSettings( - model="google/gemini-live-2.5-flash-native-audio", - voice="Charon", - frequency_penalty=None, - max_tokens=4096, - presence_penalty=None, - temperature=None, - top_k=None, - top_p=None, - seed=None, - filter_incomplete_user_turns=False, - user_turn_completion_config=None, - modalities=GeminiModalities.AUDIO, - language="en-US", - media_resolution=GeminiMediaResolution.UNSPECIFIED, - vad=None, - context_window_compression={}, - thinking={}, - enable_affective_dialog=False, - proactivity={}, - extra={}, - ) - - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - _warn_deprecated_param("model", GeminiLiveVertexLLMSettings, "model") - default_settings.model = model - if voice_id != "Charon": - _warn_deprecated_param("voice_id", GeminiLiveVertexLLMSettings, "voice") - default_settings.voice = voice_id - - # 3. Apply params overrides — only if settings not provided - if params is not None: - _warn_deprecated_param("params", GeminiLiveVertexLLMSettings) - if not settings: - default_settings.frequency_penalty = params.frequency_penalty - default_settings.max_tokens = params.max_tokens - default_settings.presence_penalty = params.presence_penalty - default_settings.temperature = params.temperature - default_settings.top_k = params.top_k - default_settings.top_p = params.top_p - default_settings.modalities = params.modalities - default_settings.language = ( - language_to_gemini_language(params.language) if params.language else "en-US" - ) - default_settings.media_resolution = params.media_resolution - default_settings.vad = params.vad - default_settings.context_window_compression = ( - params.context_window_compression.model_dump() - if params.context_window_compression - else {} - ) - default_settings.thinking = params.thinking or {} - default_settings.enable_affective_dialog = params.enable_affective_dialog or False - default_settings.proactivity = params.proactivity or {} - if isinstance(params.extra, dict): - default_settings.extra = params.extra - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) - - # Call parent constructor with the obtained settings - super().__init__( - # api_key is required by parent class, but actually not used with - # Vertex - api_key="dummy", - start_audio_paused=start_audio_paused, - start_video_paused=start_video_paused, - system_instruction=system_instruction, - tools=tools, - settings=default_settings, - inference_on_context_initialization=inference_on_context_initialization, - file_api_base_url=file_api_base_url, - http_options=http_options, - **kwargs, - ) - - def create_client(self): - """Create the Gemini client instance.""" - self._client = Client( - vertexai=True, - credentials=self._credentials, - project=self._project_id, - location=self._location, - http_options=self._http_options, - ) - - @property - def file_api(self): - """Gemini File API is not supported with Vertex AI.""" - raise NotImplementedError( - "When using Vertex AI, the recommended approach is to use Google Cloud Storage for file handling. The Gemini File API is not directly supported in this context." - ) - - @staticmethod - def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]) -> str: - """Retrieve Credentials using Google service account credentials JSON. - - Supports multiple authentication methods: - 1. Direct JSON credentials string - 2. Path to service account JSON file - 3. Default application credentials (ADC) - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - - Returns: - OAuth token for API authentication. - - Raises: - ValueError: If no valid credentials are provided or found. - """ - creds: Optional[service_account.Credentials] = None - - if credentials: - # Parse and load credentials from JSON string - creds = service_account.Credentials.from_service_account_info( - json.loads(credentials), - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - elif credentials_path: - # Load credentials from JSON file - creds = service_account.Credentials.from_service_account_file( - credentials_path, - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - else: - try: - creds, project_id = default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) - except GoogleAuthError: - pass - - if not creds: - raise ValueError("No valid credentials provided.") - - creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. - - return creds +from pipecat.services.google.gemini_live.vertex.llm import * # noqa: E402, F401, F403 diff --git a/src/pipecat/services/google/gemini_live/vertex/__init__.py b/src/pipecat/services/google/gemini_live/vertex/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/google/gemini_live/vertex/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/google/gemini_live/vertex/llm.py b/src/pipecat/services/google/gemini_live/vertex/llm.py new file mode 100644 index 000000000..cdfde1660 --- /dev/null +++ b/src/pipecat/services/google/gemini_live/vertex/llm.py @@ -0,0 +1,280 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Service for accessing Gemini Live via Google Vertex AI. + +This module provides integration with Google's Gemini Live model via +Vertex AI, supporting both text and audio modalities with voice transcription, +streaming responses, and tool usage. +""" + +import json +from dataclasses import dataclass +from typing import List, Optional, Union + +from loguru import logger + +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.services.google.gemini_live.llm import ( + GeminiLiveLLMService, + GeminiLiveLLMSettings, + GeminiMediaResolution, + GeminiModalities, + HttpOptions, + InputParams, + language_to_gemini_language, +) +from pipecat.services.settings import _warn_deprecated_param + +try: + from google.auth import default + from google.auth.exceptions import GoogleAuthError + from google.auth.transport.requests import Request + from google.genai import Client + from google.oauth2 import service_account + +except ModuleNotFoundError as e: + logger.error(f"Exception: {e}") + logger.error("In order to use Google Vertex AI, you need to `pip install pipecat-ai[google]`.") + raise Exception(f"Missing module: {e}") + + +@dataclass +class GeminiLiveVertexLLMSettings(GeminiLiveLLMSettings): + """Settings for GeminiLiveVertexLLMService.""" + + pass + + +class GeminiLiveVertexLLMService(GeminiLiveLLMService): + """Provides access to Google's Gemini Live model via Vertex AI. + + This service enables real-time conversations with Gemini, supporting both + text and audio modalities. It handles voice transcription, streaming audio + responses, and tool usage. + """ + + Settings = GeminiLiveVertexLLMSettings + _settings: GeminiLiveVertexLLMSettings + + def __init__( + self, + *, + credentials: Optional[str] = None, + credentials_path: Optional[str] = None, + location: str, + project_id: str, + model: Optional[str] = None, + voice_id: str = "Charon", + start_audio_paused: bool = False, + start_video_paused: bool = False, + system_instruction: Optional[str] = None, + tools: Optional[Union[List[dict], ToolsSchema]] = None, + params: Optional[InputParams] = None, + settings: Optional[GeminiLiveVertexLLMSettings] = None, + inference_on_context_initialization: bool = True, + file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files", + http_options: Optional[HttpOptions] = None, + **kwargs, + ): + """Initialize the service for accessing Gemini Live via Google Vertex AI. + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + location: GCP region for Vertex AI endpoint (e.g., "us-east4"). + project_id: Google Cloud project ID. + model: Model identifier to use. + + .. deprecated:: 0.0.105 + Use ``settings=GeminiLiveLLMSettings(model=...)`` instead. + + voice_id: TTS voice identifier. Defaults to "Charon". + + .. deprecated:: 0.0.105 + Use ``settings=GeminiLiveVertexLLMSettings(voice=...)`` instead. + start_audio_paused: Whether to start with audio input paused. Defaults to False. + start_video_paused: Whether to start with video input paused. Defaults to False. + system_instruction: System prompt for the model. Defaults to None. + tools: Tools/functions available to the model. Defaults to None. + params: Configuration parameters for the model along with Vertex AI + location and project ID. + + .. deprecated:: 0.0.105 + Use ``settings=GeminiLiveLLMSettings(...)`` instead. + + settings: Gemini Live LLM settings. If provided together with deprecated + top-level parameters, the ``settings`` values take precedence. + inference_on_context_initialization: Whether to generate a response when context + is first set. Defaults to True. + file_api_base_url: Base URL for the Gemini File API. Defaults to the official endpoint. + http_options: HTTP options for the client. + **kwargs: Additional arguments passed to parent GeminiLiveLLMService. + """ + # Check if user incorrectly passed api_key, which is used by parent + # class but not here. + if "api_key" in kwargs: + logger.error( + "GeminiLiveVertexLLMService does not accept 'api_key' parameter. " + "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." + ) + raise ValueError( + "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." + ) + + # These need to be set before calling super().__init__() because + # super().__init__() invokes create_client(), which needs these. + self._credentials = self._get_credentials(credentials, credentials_path) + self._project_id = project_id + self._location = location + + # Build default_settings from deprecated args, then apply settings delta. + # We pass settings= to super() instead of model=/params= to avoid + # double deprecation warnings from the parent. + + # 1. Initialize default_settings with hardcoded defaults + default_settings = GeminiLiveVertexLLMSettings( + model="google/gemini-live-2.5-flash-native-audio", + voice="Charon", + frequency_penalty=None, + max_tokens=4096, + presence_penalty=None, + temperature=None, + top_k=None, + top_p=None, + seed=None, + filter_incomplete_user_turns=False, + user_turn_completion_config=None, + modalities=GeminiModalities.AUDIO, + language="en-US", + media_resolution=GeminiMediaResolution.UNSPECIFIED, + vad=None, + context_window_compression={}, + thinking={}, + enable_affective_dialog=False, + proactivity={}, + extra={}, + ) + + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + _warn_deprecated_param("model", GeminiLiveVertexLLMSettings, "model") + default_settings.model = model + if voice_id != "Charon": + _warn_deprecated_param("voice_id", GeminiLiveVertexLLMSettings, "voice") + default_settings.voice = voice_id + + # 3. Apply params overrides — only if settings not provided + if params is not None: + _warn_deprecated_param("params", GeminiLiveVertexLLMSettings) + if not settings: + default_settings.frequency_penalty = params.frequency_penalty + default_settings.max_tokens = params.max_tokens + default_settings.presence_penalty = params.presence_penalty + default_settings.temperature = params.temperature + default_settings.top_k = params.top_k + default_settings.top_p = params.top_p + default_settings.modalities = params.modalities + default_settings.language = ( + language_to_gemini_language(params.language) if params.language else "en-US" + ) + default_settings.media_resolution = params.media_resolution + default_settings.vad = params.vad + default_settings.context_window_compression = ( + params.context_window_compression.model_dump() + if params.context_window_compression + else {} + ) + default_settings.thinking = params.thinking or {} + default_settings.enable_affective_dialog = params.enable_affective_dialog or False + default_settings.proactivity = params.proactivity or {} + if isinstance(params.extra, dict): + default_settings.extra = params.extra + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + # Call parent constructor with the obtained settings + super().__init__( + # api_key is required by parent class, but actually not used with + # Vertex + api_key="dummy", + start_audio_paused=start_audio_paused, + start_video_paused=start_video_paused, + system_instruction=system_instruction, + tools=tools, + settings=default_settings, + inference_on_context_initialization=inference_on_context_initialization, + file_api_base_url=file_api_base_url, + http_options=http_options, + **kwargs, + ) + + def create_client(self): + """Create the Gemini client instance.""" + self._client = Client( + vertexai=True, + credentials=self._credentials, + project=self._project_id, + location=self._location, + http_options=self._http_options, + ) + + @property + def file_api(self): + """Gemini File API is not supported with Vertex AI.""" + raise NotImplementedError( + "When using Vertex AI, the recommended approach is to use Google Cloud Storage for file handling. The Gemini File API is not directly supported in this context." + ) + + @staticmethod + def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]) -> str: + """Retrieve Credentials using Google service account credentials JSON. + + Supports multiple authentication methods: + 1. Direct JSON credentials string + 2. Path to service account JSON file + 3. Default application credentials (ADC) + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + + Returns: + OAuth token for API authentication. + + Raises: + ValueError: If no valid credentials are provided or found. + """ + creds: Optional[service_account.Credentials] = None + + if credentials: + # Parse and load credentials from JSON string + creds = service_account.Credentials.from_service_account_info( + json.loads(credentials), + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + elif credentials_path: + # Load credentials from JSON file + creds = service_account.Credentials.from_service_account_file( + credentials_path, + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + else: + try: + creds, project_id = default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + except GoogleAuthError: + pass + + if not creds: + raise ValueError("No valid credentials provided.") + + creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. + + return creds diff --git a/src/pipecat/services/google/google.py b/src/pipecat/services/google/google.py index 3d1814cf0..b2fc88b23 100644 --- a/src/pipecat/services/google/google.py +++ b/src/pipecat/services/google/google.py @@ -13,12 +13,12 @@ from pipecat.services import DeprecatedModuleProxy from .frames import * from .image import * from .llm import * -from .llm_openai import * -from .llm_vertex import * +from .openai import * from .rtvi import * from .stt import * from .tts import * +from .vertex import * sys.modules[__name__] = DeprecatedModuleProxy( - globals(), "google", "google.[frames,image,llm,llm_openai,llm_vertex,rtvi,stt,tts]" + globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]" ) diff --git a/src/pipecat/services/google/llm_openai.py b/src/pipecat/services/google/llm_openai.py index cd5e0f060..f9d182e78 100644 --- a/src/pipecat/services/google/llm_openai.py +++ b/src/pipecat/services/google/llm_openai.py @@ -4,211 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Google LLM service using OpenAI-compatible API format. +"""Deprecated: use ``pipecat.services.google.openai.llm`` instead.""" -This module provides integration with Google's AI LLM models using the OpenAI -API format through Google's Gemini API OpenAI compatibility layer. -""" +import warnings -import json -import os -from dataclasses import dataclass -from typing import Optional +warnings.warn( + "Module `pipecat.services.google.llm_openai` is deprecated, " + "use `pipecat.services.google.openai.llm` instead.", + DeprecationWarning, + stacklevel=2, +) -from openai import AsyncStream -from openai.types.chat import ChatCompletionChunk - -from pipecat.services.llm_service import FunctionCallFromLLM - -# Suppress gRPC fork warnings -os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" - -from loguru import logger - -from pipecat.frames.frames import LLMTextFrame -from pipecat.metrics.metrics import LLMTokenUsage -from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openai.base_llm import OpenAILLMSettings -from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param - - -@dataclass -class GoogleOpenAILLMSettings(OpenAILLMSettings): - """Settings for GoogleLLMOpenAIBetaService.""" - - pass - - -class GoogleLLMOpenAIBetaService(OpenAILLMService): - """Google LLM service using OpenAI-compatible API format. - - This service provides access to Google's AI LLM models (like Gemini) through - the OpenAI API format. It handles streaming responses, function calls, and - tool usage while maintaining compatibility with OpenAI's interface. - - Note: This service includes a workaround for a Google API bug where function - call indices may be incorrectly set to None, resulting in empty function names. - - .. deprecated:: 0.0.82 - GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. - Use GoogleLLMService instead for better integration with Google's native API. - - Reference: - https://ai.google.dev/gemini-api/docs/openai - """ - - Settings = GoogleOpenAILLMSettings - _settings: GoogleOpenAILLMSettings - - def __init__( - self, - *, - api_key: str, - base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/", - model: Optional[str] = None, - settings: Optional[GoogleOpenAILLMSettings] = None, - **kwargs, - ): - """Initialize the Google LLM service. - - Args: - api_key: Google API key for authentication. - base_url: Base URL for Google's OpenAI-compatible API. - model: Google model name to use (e.g., "gemini-2.0-flash"). - - .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. - - settings: Runtime-updatable settings. When provided alongside deprecated - parameters, ``settings`` values take precedence. - **kwargs: Additional arguments passed to the parent OpenAILLMService. - """ - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " - "Use GoogleLLMService instead for better integration with Google's native API.", - DeprecationWarning, - stacklevel=2, - ) - - # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleOpenAILLMSettings(model="gemini-2.0-flash") - - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - _warn_deprecated_param("model", GoogleOpenAILLMSettings, "model") - default_settings.model = model - - # 3. (No step 3, as there's no params object to apply) - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) - - super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs) - - async def _process_context(self, context: OpenAILLMContext): - functions_list = [] - arguments_list = [] - tool_id_list = [] - func_idx = 0 - function_name = "" - arguments = "" - tool_call_id = "" - - await self.start_ttfb_metrics() - - chunk_stream: AsyncStream[ - ChatCompletionChunk - ] = await self._stream_chat_completions_specific_context(context) - - # Use context manager to ensure stream is closed on cancellation/exception. - # Without this, CancelledError during iteration leaves the underlying socket open. - async with chunk_stream: - async for chunk in chunk_stream: - if chunk.usage: - tokens = LLMTokenUsage( - prompt_tokens=chunk.usage.prompt_tokens or 0, - completion_tokens=chunk.usage.completion_tokens or 0, - total_tokens=chunk.usage.total_tokens or 0, - ) - await self.start_llm_usage_metrics(tokens) - - if chunk.choices is None or len(chunk.choices) == 0: - continue - - await self.stop_ttfb_metrics() - - if not chunk.choices[0].delta: - continue - - if chunk.choices[0].delta.tool_calls: - # We're streaming the LLM response to enable the fastest response times. - # For text, we just yield each chunk as we receive it and count on consumers - # to do whatever coalescing they need (eg. to pass full sentences to TTS) - # - # If the LLM is a function call, we'll do some coalescing here. - # If the response contains a function name, we'll yield a frame to tell consumers - # that they can start preparing to call the function with that name. - # We accumulate all the arguments for the rest of the streamed response, then when - # the response is done, we package up all the arguments and the function name and - # yield a frame containing the function name and the arguments. - logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}") - tool_call = chunk.choices[0].delta.tool_calls[0] - if tool_call.index != func_idx: - functions_list.append(function_name) - arguments_list.append(arguments) - tool_id_list.append(tool_call_id) - function_name = "" - arguments = "" - tool_call_id = "" - func_idx += 1 - if tool_call.function and tool_call.function.name: - function_name += tool_call.function.name - tool_call_id = tool_call.id - if tool_call.function and tool_call.function.arguments: - # Keep iterating through the response to collect all the argument fragments - arguments += tool_call.function.arguments - elif chunk.choices[0].delta.content: - await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content)) - - # if we got a function name and arguments, check to see if it's a function with - # a registered handler. If so, run the registered callback, save the result to - # the context, and re-prompt to get a chat answer. If we don't have a registered - # handler, raise an exception. - if function_name and arguments: - # added to the list as last function name and arguments not added to the list - functions_list.append(function_name) - arguments_list.append(arguments) - tool_id_list.append(tool_call_id) - - logger.debug( - f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}" - ) - - function_calls = [] - for function_name, arguments, tool_id in zip( - functions_list, arguments_list, tool_id_list - ): - if function_name == "": - # TODO: Remove the _process_context method once Google resolves the bug - # where the index is incorrectly set to None instead of returning the actual index, - # which currently results in an empty function name(''). - continue - - arguments = json.loads(arguments) - - function_calls.append( - FunctionCallFromLLM( - context=context, - tool_call_id=tool_id, - function_name=function_name, - arguments=arguments, - ) - ) - - await self.run_function_calls(function_calls) +from pipecat.services.google.openai.llm import * # noqa: E402, F401, F403 diff --git a/src/pipecat/services/google/llm_vertex.py b/src/pipecat/services/google/llm_vertex.py index 3901b5d50..54d338ad7 100644 --- a/src/pipecat/services/google/llm_vertex.py +++ b/src/pipecat/services/google/llm_vertex.py @@ -4,306 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Google Vertex AI LLM service implementation. +"""Deprecated: use ``pipecat.services.google.vertex.llm`` instead.""" -This module provides integration with Google's AI models via Vertex AI, -extending the GoogleLLMService with Vertex AI authentication. -""" +import warnings -import json -import os -from dataclasses import dataclass +warnings.warn( + "Module `pipecat.services.google.llm_vertex` is deprecated, " + "use `pipecat.services.google.vertex.llm` instead.", + DeprecationWarning, + stacklevel=2, +) -# Suppress gRPC fork warnings -os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" - -from typing import Optional - -from loguru import logger - -from pipecat.services.google.llm import GoogleLLMService, GoogleLLMSettings -from pipecat.services.settings import _warn_deprecated_param - -try: - from google.auth import default - from google.auth.exceptions import GoogleAuthError - from google.auth.transport.requests import Request - from google.genai import Client - from google.genai.types import HttpOptions - from google.oauth2 import service_account - -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error( - "In order to use Google AI, you need to `pip install pipecat-ai[google]`. Also, set `GOOGLE_APPLICATION_CREDENTIALS` environment variable." - ) - raise Exception(f"Missing module: {e}") - - -@dataclass -class GoogleVertexLLMSettings(GoogleLLMSettings): - """Settings for GoogleVertexLLMService.""" - - pass - - -class GoogleVertexLLMService(GoogleLLMService): - """Google Vertex AI LLM service extending GoogleLLMService. - - Provides access to Google's AI models via Vertex AI while using the same - Google AI client and message format as GoogleLLMService. Handles authentication - using Google service account credentials and configures the client for - Vertex AI endpoints. - - Reference: - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference - """ - - Settings = GoogleVertexLLMSettings - _settings: GoogleVertexLLMSettings - - class InputParams(GoogleLLMService.InputParams): - """Input parameters specific to Vertex AI. - - Parameters: - location: GCP region for Vertex AI endpoint (e.g., "us-east4"). - - .. deprecated:: 0.0.90 - Use `location` as a direct argument to - `GoogleVertexLLMService.__init__()` instead. - - project_id: Google Cloud project ID. - - .. deprecated:: 0.0.90 - Use `project_id` as a direct argument to - `GoogleVertexLLMService.__init__()` instead. - """ - - # https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations - location: Optional[str] = None - project_id: Optional[str] = None - - def __init__(self, **kwargs): - """Initializes the InputParams.""" - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - if "location" in kwargs and kwargs["location"] is not None: - warnings.warn( - "GoogleVertexLLMService.InputParams.location is deprecated. " - "Please provide 'location' as a direct argument to GoogleVertexLLMService.__init__() instead.", - DeprecationWarning, - stacklevel=2, - ) - - if "project_id" in kwargs and kwargs["project_id"] is not None: - warnings.warn( - "GoogleVertexLLMService.InputParams.project_id is deprecated. " - "Please provide 'project_id' as a direct argument to GoogleVertexLLMService.__init__() instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(**kwargs) - - def __init__( - self, - *, - credentials: Optional[str] = None, - credentials_path: Optional[str] = None, - model: Optional[str] = None, - location: Optional[str] = None, - project_id: Optional[str] = None, - params: Optional[GoogleLLMService.InputParams] = None, - settings: Optional[GoogleVertexLLMSettings] = None, - system_instruction: Optional[str] = None, - tools: Optional[list] = None, - tool_config: Optional[dict] = None, - http_options: Optional[HttpOptions] = None, - **kwargs, - ): - """Initializes the VertexLLMService. - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - model: Model identifier (e.g., "gemini-2.5-flash"). - - .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(model=...)`` instead. - - location: GCP region for Vertex AI endpoint (e.g., "us-east4"). - project_id: Google Cloud project ID. - params: Input parameters for the model. - - .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(...)`` instead. - - settings: Runtime-updatable settings for this service. When both - deprecated parameters and *settings* are provided, *settings* - values take precedence. - system_instruction: System instruction/prompt for the model. - - .. deprecated:: 0.0.105 - Use ``settings=GoogleVertexLLMSettings(system_instruction=...)`` instead. - tools: List of available tools/functions. - tool_config: Configuration for tool usage. - http_options: HTTP options for the client. - **kwargs: Additional arguments passed to GoogleLLMService. - """ - # Check if user incorrectly passed api_key, which is used by parent - # class but not here. - if "api_key" in kwargs: - logger.error( - "GoogleVertexLLMService does not accept 'api_key' parameter. " - "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." - ) - raise ValueError( - "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." - ) - - # Handle deprecated InputParams fields (location/project_id extraction - # must happen before validation, regardless of settings) - if params and isinstance(params, GoogleVertexLLMService.InputParams): - if project_id is None: - project_id = params.project_id - if location is None: - location = params.location - # Convert to base InputParams - params = GoogleLLMService.InputParams( - **params.model_dump(exclude={"location", "project_id"}, exclude_unset=True) - ) - - # Validate project_id and location parameters - # NOTE: once we remove Vertex-specific InputParams class, we can update - # __init__() signature as follows: - # - location: str = "us-east4", - # - project_id: str, - # But for now, we need them as-is to maintain proper backward - # compatibility. - if project_id is None: - raise ValueError("project_id is required") - if location is None: - # If location is not provided, default to "us-east4". - # Note: this is legacy behavior; ideally location would be - # required. - logger.warning("location is not provided. Defaulting to 'us-east4'.") - location = "us-east4" # Default location if not provided - - # These need to be set before calling super().__init__() because - # super().__init__() invokes _create_client(), which needs these. - self._credentials = self._get_credentials(credentials, credentials_path) - self._project_id = project_id - self._location = location - - # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleVertexLLMSettings( - model="gemini-2.5-flash", - system_instruction=None, - max_tokens=4096, - temperature=None, - top_k=None, - top_p=None, - frequency_penalty=None, - presence_penalty=None, - seed=None, - filter_incomplete_user_turns=False, - user_turn_completion_config=None, - thinking=None, - extra={}, - ) - - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - _warn_deprecated_param("model", GoogleVertexLLMSettings, "model") - default_settings.model = model - if system_instruction is not None: - _warn_deprecated_param( - "system_instruction", GoogleVertexLLMSettings, "system_instruction" - ) - default_settings.system_instruction = system_instruction - - # 3. Apply params overrides — only if settings not provided - if params is not None: - _warn_deprecated_param("params", GoogleVertexLLMSettings) - if not settings: - default_settings.max_tokens = params.max_tokens - default_settings.temperature = params.temperature - default_settings.top_k = params.top_k - default_settings.top_p = params.top_p - default_settings.thinking = params.thinking - if isinstance(params.extra, dict): - default_settings.extra = params.extra - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) - - # Call parent constructor with dummy api_key - # (api_key is required by parent class, but not actually used with Vertex) - super().__init__( - api_key="dummy", - settings=default_settings, - tools=tools, - tool_config=tool_config, - http_options=http_options, - **kwargs, - ) - - def create_client(self): - """Create the Gemini client instance configured for Vertex AI.""" - self._client = Client( - vertexai=True, - credentials=self._credentials, - project=self._project_id, - location=self._location, - http_options=self._http_options, - ) - - @staticmethod - def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]): - """Retrieve Credentials using Google service account credentials. - - Supports multiple authentication methods: - 1. Direct JSON credentials string - 2. Path to service account JSON file - 3. Default application credentials (ADC) - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - - Returns: - Google credentials object for API authentication. - - Raises: - ValueError: If no valid credentials are provided or found. - """ - creds: Optional[service_account.Credentials] = None - - if credentials: - # Parse and load credentials from JSON string - creds = service_account.Credentials.from_service_account_info( - json.loads(credentials), - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - elif credentials_path: - # Load credentials from JSON file - creds = service_account.Credentials.from_service_account_file( - credentials_path, - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - else: - try: - creds, project_id = default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) - except GoogleAuthError: - pass - - if not creds: - raise ValueError("No valid credentials provided.") - - creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. - - return creds +from pipecat.services.google.vertex.llm import * # noqa: E402, F401, F403 diff --git a/src/pipecat/services/google/openai/__init__.py b/src/pipecat/services/google/openai/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/google/openai/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/google/openai/llm.py b/src/pipecat/services/google/openai/llm.py new file mode 100644 index 000000000..cd5e0f060 --- /dev/null +++ b/src/pipecat/services/google/openai/llm.py @@ -0,0 +1,214 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Google LLM service using OpenAI-compatible API format. + +This module provides integration with Google's AI LLM models using the OpenAI +API format through Google's Gemini API OpenAI compatibility layer. +""" + +import json +import os +from dataclasses import dataclass +from typing import Optional + +from openai import AsyncStream +from openai.types.chat import ChatCompletionChunk + +from pipecat.services.llm_service import FunctionCallFromLLM + +# Suppress gRPC fork warnings +os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" + +from loguru import logger + +from pipecat.frames.frames import LLMTextFrame +from pipecat.metrics.metrics import LLMTokenUsage +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.settings import _warn_deprecated_param + + +@dataclass +class GoogleOpenAILLMSettings(OpenAILLMSettings): + """Settings for GoogleLLMOpenAIBetaService.""" + + pass + + +class GoogleLLMOpenAIBetaService(OpenAILLMService): + """Google LLM service using OpenAI-compatible API format. + + This service provides access to Google's AI LLM models (like Gemini) through + the OpenAI API format. It handles streaming responses, function calls, and + tool usage while maintaining compatibility with OpenAI's interface. + + Note: This service includes a workaround for a Google API bug where function + call indices may be incorrectly set to None, resulting in empty function names. + + .. deprecated:: 0.0.82 + GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. + Use GoogleLLMService instead for better integration with Google's native API. + + Reference: + https://ai.google.dev/gemini-api/docs/openai + """ + + Settings = GoogleOpenAILLMSettings + _settings: GoogleOpenAILLMSettings + + def __init__( + self, + *, + api_key: str, + base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/", + model: Optional[str] = None, + settings: Optional[GoogleOpenAILLMSettings] = None, + **kwargs, + ): + """Initialize the Google LLM service. + + Args: + api_key: Google API key for authentication. + base_url: Base URL for Google's OpenAI-compatible API. + model: Google model name to use (e.g., "gemini-2.0-flash"). + + .. deprecated:: 0.0.105 + Use ``settings=OpenAILLMSettings(model=...)`` instead. + + settings: Runtime-updatable settings. When provided alongside deprecated + parameters, ``settings`` values take precedence. + **kwargs: Additional arguments passed to the parent OpenAILLMService. + """ + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " + "Use GoogleLLMService instead for better integration with Google's native API.", + DeprecationWarning, + stacklevel=2, + ) + + # 1. Initialize default_settings with hardcoded defaults + default_settings = GoogleOpenAILLMSettings(model="gemini-2.0-flash") + + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + _warn_deprecated_param("model", GoogleOpenAILLMSettings, "model") + default_settings.model = model + + # 3. (No step 3, as there's no params object to apply) + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs) + + async def _process_context(self, context: OpenAILLMContext): + functions_list = [] + arguments_list = [] + tool_id_list = [] + func_idx = 0 + function_name = "" + arguments = "" + tool_call_id = "" + + await self.start_ttfb_metrics() + + chunk_stream: AsyncStream[ + ChatCompletionChunk + ] = await self._stream_chat_completions_specific_context(context) + + # Use context manager to ensure stream is closed on cancellation/exception. + # Without this, CancelledError during iteration leaves the underlying socket open. + async with chunk_stream: + async for chunk in chunk_stream: + if chunk.usage: + tokens = LLMTokenUsage( + prompt_tokens=chunk.usage.prompt_tokens or 0, + completion_tokens=chunk.usage.completion_tokens or 0, + total_tokens=chunk.usage.total_tokens or 0, + ) + await self.start_llm_usage_metrics(tokens) + + if chunk.choices is None or len(chunk.choices) == 0: + continue + + await self.stop_ttfb_metrics() + + if not chunk.choices[0].delta: + continue + + if chunk.choices[0].delta.tool_calls: + # We're streaming the LLM response to enable the fastest response times. + # For text, we just yield each chunk as we receive it and count on consumers + # to do whatever coalescing they need (eg. to pass full sentences to TTS) + # + # If the LLM is a function call, we'll do some coalescing here. + # If the response contains a function name, we'll yield a frame to tell consumers + # that they can start preparing to call the function with that name. + # We accumulate all the arguments for the rest of the streamed response, then when + # the response is done, we package up all the arguments and the function name and + # yield a frame containing the function name and the arguments. + logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}") + tool_call = chunk.choices[0].delta.tool_calls[0] + if tool_call.index != func_idx: + functions_list.append(function_name) + arguments_list.append(arguments) + tool_id_list.append(tool_call_id) + function_name = "" + arguments = "" + tool_call_id = "" + func_idx += 1 + if tool_call.function and tool_call.function.name: + function_name += tool_call.function.name + tool_call_id = tool_call.id + if tool_call.function and tool_call.function.arguments: + # Keep iterating through the response to collect all the argument fragments + arguments += tool_call.function.arguments + elif chunk.choices[0].delta.content: + await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content)) + + # if we got a function name and arguments, check to see if it's a function with + # a registered handler. If so, run the registered callback, save the result to + # the context, and re-prompt to get a chat answer. If we don't have a registered + # handler, raise an exception. + if function_name and arguments: + # added to the list as last function name and arguments not added to the list + functions_list.append(function_name) + arguments_list.append(arguments) + tool_id_list.append(tool_call_id) + + logger.debug( + f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}" + ) + + function_calls = [] + for function_name, arguments, tool_id in zip( + functions_list, arguments_list, tool_id_list + ): + if function_name == "": + # TODO: Remove the _process_context method once Google resolves the bug + # where the index is incorrectly set to None instead of returning the actual index, + # which currently results in an empty function name(''). + continue + + arguments = json.loads(arguments) + + function_calls.append( + FunctionCallFromLLM( + context=context, + tool_call_id=tool_id, + function_name=function_name, + arguments=arguments, + ) + ) + + await self.run_function_calls(function_calls) diff --git a/src/pipecat/services/google/vertex/__init__.py b/src/pipecat/services/google/vertex/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/google/vertex/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/google/vertex/llm.py b/src/pipecat/services/google/vertex/llm.py new file mode 100644 index 000000000..3901b5d50 --- /dev/null +++ b/src/pipecat/services/google/vertex/llm.py @@ -0,0 +1,309 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Google Vertex AI LLM service implementation. + +This module provides integration with Google's AI models via Vertex AI, +extending the GoogleLLMService with Vertex AI authentication. +""" + +import json +import os +from dataclasses import dataclass + +# Suppress gRPC fork warnings +os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" + +from typing import Optional + +from loguru import logger + +from pipecat.services.google.llm import GoogleLLMService, GoogleLLMSettings +from pipecat.services.settings import _warn_deprecated_param + +try: + from google.auth import default + from google.auth.exceptions import GoogleAuthError + from google.auth.transport.requests import Request + from google.genai import Client + from google.genai.types import HttpOptions + from google.oauth2 import service_account + +except ModuleNotFoundError as e: + logger.error(f"Exception: {e}") + logger.error( + "In order to use Google AI, you need to `pip install pipecat-ai[google]`. Also, set `GOOGLE_APPLICATION_CREDENTIALS` environment variable." + ) + raise Exception(f"Missing module: {e}") + + +@dataclass +class GoogleVertexLLMSettings(GoogleLLMSettings): + """Settings for GoogleVertexLLMService.""" + + pass + + +class GoogleVertexLLMService(GoogleLLMService): + """Google Vertex AI LLM service extending GoogleLLMService. + + Provides access to Google's AI models via Vertex AI while using the same + Google AI client and message format as GoogleLLMService. Handles authentication + using Google service account credentials and configures the client for + Vertex AI endpoints. + + Reference: + https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference + """ + + Settings = GoogleVertexLLMSettings + _settings: GoogleVertexLLMSettings + + class InputParams(GoogleLLMService.InputParams): + """Input parameters specific to Vertex AI. + + Parameters: + location: GCP region for Vertex AI endpoint (e.g., "us-east4"). + + .. deprecated:: 0.0.90 + Use `location` as a direct argument to + `GoogleVertexLLMService.__init__()` instead. + + project_id: Google Cloud project ID. + + .. deprecated:: 0.0.90 + Use `project_id` as a direct argument to + `GoogleVertexLLMService.__init__()` instead. + """ + + # https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations + location: Optional[str] = None + project_id: Optional[str] = None + + def __init__(self, **kwargs): + """Initializes the InputParams.""" + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + if "location" in kwargs and kwargs["location"] is not None: + warnings.warn( + "GoogleVertexLLMService.InputParams.location is deprecated. " + "Please provide 'location' as a direct argument to GoogleVertexLLMService.__init__() instead.", + DeprecationWarning, + stacklevel=2, + ) + + if "project_id" in kwargs and kwargs["project_id"] is not None: + warnings.warn( + "GoogleVertexLLMService.InputParams.project_id is deprecated. " + "Please provide 'project_id' as a direct argument to GoogleVertexLLMService.__init__() instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(**kwargs) + + def __init__( + self, + *, + credentials: Optional[str] = None, + credentials_path: Optional[str] = None, + model: Optional[str] = None, + location: Optional[str] = None, + project_id: Optional[str] = None, + params: Optional[GoogleLLMService.InputParams] = None, + settings: Optional[GoogleVertexLLMSettings] = None, + system_instruction: Optional[str] = None, + tools: Optional[list] = None, + tool_config: Optional[dict] = None, + http_options: Optional[HttpOptions] = None, + **kwargs, + ): + """Initializes the VertexLLMService. + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + model: Model identifier (e.g., "gemini-2.5-flash"). + + .. deprecated:: 0.0.105 + Use ``settings=GoogleLLMSettings(model=...)`` instead. + + location: GCP region for Vertex AI endpoint (e.g., "us-east4"). + project_id: Google Cloud project ID. + params: Input parameters for the model. + + .. deprecated:: 0.0.105 + Use ``settings=GoogleLLMSettings(...)`` instead. + + settings: Runtime-updatable settings for this service. When both + deprecated parameters and *settings* are provided, *settings* + values take precedence. + system_instruction: System instruction/prompt for the model. + + .. deprecated:: 0.0.105 + Use ``settings=GoogleVertexLLMSettings(system_instruction=...)`` instead. + tools: List of available tools/functions. + tool_config: Configuration for tool usage. + http_options: HTTP options for the client. + **kwargs: Additional arguments passed to GoogleLLMService. + """ + # Check if user incorrectly passed api_key, which is used by parent + # class but not here. + if "api_key" in kwargs: + logger.error( + "GoogleVertexLLMService does not accept 'api_key' parameter. " + "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." + ) + raise ValueError( + "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." + ) + + # Handle deprecated InputParams fields (location/project_id extraction + # must happen before validation, regardless of settings) + if params and isinstance(params, GoogleVertexLLMService.InputParams): + if project_id is None: + project_id = params.project_id + if location is None: + location = params.location + # Convert to base InputParams + params = GoogleLLMService.InputParams( + **params.model_dump(exclude={"location", "project_id"}, exclude_unset=True) + ) + + # Validate project_id and location parameters + # NOTE: once we remove Vertex-specific InputParams class, we can update + # __init__() signature as follows: + # - location: str = "us-east4", + # - project_id: str, + # But for now, we need them as-is to maintain proper backward + # compatibility. + if project_id is None: + raise ValueError("project_id is required") + if location is None: + # If location is not provided, default to "us-east4". + # Note: this is legacy behavior; ideally location would be + # required. + logger.warning("location is not provided. Defaulting to 'us-east4'.") + location = "us-east4" # Default location if not provided + + # These need to be set before calling super().__init__() because + # super().__init__() invokes _create_client(), which needs these. + self._credentials = self._get_credentials(credentials, credentials_path) + self._project_id = project_id + self._location = location + + # 1. Initialize default_settings with hardcoded defaults + default_settings = GoogleVertexLLMSettings( + model="gemini-2.5-flash", + system_instruction=None, + max_tokens=4096, + temperature=None, + top_k=None, + top_p=None, + frequency_penalty=None, + presence_penalty=None, + seed=None, + filter_incomplete_user_turns=False, + user_turn_completion_config=None, + thinking=None, + extra={}, + ) + + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + _warn_deprecated_param("model", GoogleVertexLLMSettings, "model") + default_settings.model = model + if system_instruction is not None: + _warn_deprecated_param( + "system_instruction", GoogleVertexLLMSettings, "system_instruction" + ) + default_settings.system_instruction = system_instruction + + # 3. Apply params overrides — only if settings not provided + if params is not None: + _warn_deprecated_param("params", GoogleVertexLLMSettings) + if not settings: + default_settings.max_tokens = params.max_tokens + default_settings.temperature = params.temperature + default_settings.top_k = params.top_k + default_settings.top_p = params.top_p + default_settings.thinking = params.thinking + if isinstance(params.extra, dict): + default_settings.extra = params.extra + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + # Call parent constructor with dummy api_key + # (api_key is required by parent class, but not actually used with Vertex) + super().__init__( + api_key="dummy", + settings=default_settings, + tools=tools, + tool_config=tool_config, + http_options=http_options, + **kwargs, + ) + + def create_client(self): + """Create the Gemini client instance configured for Vertex AI.""" + self._client = Client( + vertexai=True, + credentials=self._credentials, + project=self._project_id, + location=self._location, + http_options=self._http_options, + ) + + @staticmethod + def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]): + """Retrieve Credentials using Google service account credentials. + + Supports multiple authentication methods: + 1. Direct JSON credentials string + 2. Path to service account JSON file + 3. Default application credentials (ADC) + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + + Returns: + Google credentials object for API authentication. + + Raises: + ValueError: If no valid credentials are provided or found. + """ + creds: Optional[service_account.Credentials] = None + + if credentials: + # Parse and load credentials from JSON string + creds = service_account.Credentials.from_service_account_info( + json.loads(credentials), + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + elif credentials_path: + # Load credentials from JSON file + creds = service_account.Credentials.from_service_account_file( + credentials_path, + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + else: + try: + creds, project_id = default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + except GoogleAuthError: + pass + + if not creds: + raise ValueError("No valid credentials provided.") + + creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. + + return creds diff --git a/tests/test_google_llm_openai.py b/tests/test_google_llm_openai.py index 2940bf7d7..5e6cee6f8 100644 --- a/tests/test_google_llm_openai.py +++ b/tests/test_google_llm_openai.py @@ -15,7 +15,7 @@ import pytest from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext try: - from pipecat.services.google.llm_openai import GoogleLLMOpenAIBetaService + from pipecat.services.google.openai.llm import GoogleLLMOpenAIBetaService google_available = True except Exception: