From af089a65ae9bbadecdf178da45a6c9c0577df2de Mon Sep 17 00:00:00 2001 From: thorwebdev <5748289+thorwebdev@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:06:28 -0500 Subject: [PATCH 1/6] feat: add Gemini client identification. --- .../services/google/gemini_live/llm.py | 16 +++++++++++++ .../services/google/gemini_live/llm_vertex.py | 1 + src/pipecat/services/google/image.py | 23 +++++++++++++++++-- src/pipecat/services/google/llm.py | 16 +++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 19bcda730..ed242aa81 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -24,6 +24,7 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field +from pipecat import __version__ from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.frames.frames import ( @@ -682,6 +683,21 @@ class GeminiLiveLLMService(LLMService): self._context = None self._api_key = api_key self._http_options = http_options + + # Add client header + client_header = {"x-goog-api-client": f"pipecat/{__version__}"} + if self._http_options is None: + self._http_options = {"headers": client_header} + elif isinstance(self._http_options, dict): + if "headers" in self._http_options: + self._http_options["headers"].update(client_header) + else: + self._http_options["headers"] = client_header + elif hasattr(self._http_options, "headers"): + if self._http_options.headers is None: + self._http_options.headers = client_header + else: + self._http_options.headers.update(client_header) self._session: AsyncSession = None self._connection_task = None diff --git a/src/pipecat/services/google/gemini_live/llm_vertex.py b/src/pipecat/services/google/gemini_live/llm_vertex.py index a38154755..c58ef311d 100644 --- a/src/pipecat/services/google/gemini_live/llm_vertex.py +++ b/src/pipecat/services/google/gemini_live/llm_vertex.py @@ -126,6 +126,7 @@ class GeminiLiveVertexLLMService(GeminiLiveLLMService): credentials=self._credentials, project=self._project_id, location=self._location, + http_options=self._http_options, ) @property diff --git a/src/pipecat/services/google/image.py b/src/pipecat/services/google/image.py index f92c7bb2b..030fe7ee5 100644 --- a/src/pipecat/services/google/image.py +++ b/src/pipecat/services/google/image.py @@ -16,12 +16,13 @@ import os # Suppress gRPC fork warnings os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger from PIL import Image from pydantic import BaseModel, Field +from pipecat import __version__ from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame from pipecat.services.image_service import ImageGenService @@ -60,6 +61,7 @@ class GoogleImageGenService(ImageGenService): *, api_key: str, params: Optional[InputParams] = None, + http_options: Optional[Any] = None, **kwargs, ): """Initialize the GoogleImageGenService with API key and parameters. @@ -67,11 +69,28 @@ class GoogleImageGenService(ImageGenService): Args: api_key: Google AI API key for authentication. params: Configuration parameters for image generation. Defaults to InputParams(). + http_options: HTTP options for the client. **kwargs: Additional arguments passed to the parent ImageGenService. """ super().__init__(**kwargs) self._params = params or GoogleImageGenService.InputParams() - self._client = genai.Client(api_key=api_key) + + # Add client header + client_header = {"x-goog-api-client": f"pipecat/{__version__}"} + if http_options is None: + http_options = {"headers": client_header} + elif isinstance(http_options, dict): + if "headers" in http_options: + http_options["headers"].update(client_header) + else: + http_options["headers"] = client_header + elif hasattr(http_options, "headers"): + if http_options.headers is None: + http_options.headers = client_header + else: + http_options.headers.update(client_header) + + self._client = genai.Client(api_key=api_key, http_options=http_options) self.set_model_name(self._params.model) def can_generate_metrics(self) -> bool: diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index 840b473b2..f28a4c1b0 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -22,6 +22,7 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field +from pipecat import __version__ from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter, GeminiLLMInvocationParams from pipecat.frames.frames import ( AudioRawFrame, @@ -714,6 +715,21 @@ class GoogleLLMService(LLMService): self._api_key = api_key self._system_instruction = system_instruction self._http_options = http_options + + # Add client header + client_header = {"x-goog-api-client": f"pipecat/{__version__}"} + if self._http_options is None: + self._http_options = {"headers": client_header} + elif isinstance(self._http_options, dict): + if "headers" in self._http_options: + self._http_options["headers"].update(client_header) + else: + self._http_options["headers"] = client_header + elif hasattr(self._http_options, "headers"): + if self._http_options.headers is None: + self._http_options.headers = client_header + else: + self._http_options.headers.update(client_header) self._settings = { "max_tokens": params.max_tokens, From f729b1625bc133b2c7667fb4fa1e1741e4b3041e Mon Sep 17 00:00:00 2001 From: thorwebdev <5748289+thorwebdev@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:31:58 -0500 Subject: [PATCH 2/6] chore: move into services file. --- .../services/google/gemini_live/llm.py | 19 +-------- src/pipecat/services/google/image.py | 17 ++------ src/pipecat/services/google/llm.py | 19 +-------- src/pipecat/services/google/utils.py | 41 +++++++++++++++++++ 4 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 src/pipecat/services/google/utils.py diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index ed242aa81..cb1ca7d1e 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -24,7 +24,7 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field -from pipecat import __version__ +from pipecat.services.google.utils import update_google_client_http_options from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.frames.frames import ( @@ -682,22 +682,7 @@ class GeminiLiveLLMService(LLMService): self._video_input_paused = start_video_paused self._context = None self._api_key = api_key - self._http_options = http_options - - # Add client header - client_header = {"x-goog-api-client": f"pipecat/{__version__}"} - if self._http_options is None: - self._http_options = {"headers": client_header} - elif isinstance(self._http_options, dict): - if "headers" in self._http_options: - self._http_options["headers"].update(client_header) - else: - self._http_options["headers"] = client_header - elif hasattr(self._http_options, "headers"): - if self._http_options.headers is None: - self._http_options.headers = client_header - else: - self._http_options.headers.update(client_header) + self._http_options = update_google_client_http_options(http_options) self._session: AsyncSession = None self._connection_task = None diff --git a/src/pipecat/services/google/image.py b/src/pipecat/services/google/image.py index 030fe7ee5..9b4e1c615 100644 --- a/src/pipecat/services/google/image.py +++ b/src/pipecat/services/google/image.py @@ -22,7 +22,8 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field -from pipecat import __version__ + +from pipecat.services.google.utils import update_google_client_http_options from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame from pipecat.services.image_service import ImageGenService @@ -76,19 +77,7 @@ class GoogleImageGenService(ImageGenService): self._params = params or GoogleImageGenService.InputParams() # Add client header - client_header = {"x-goog-api-client": f"pipecat/{__version__}"} - if http_options is None: - http_options = {"headers": client_header} - elif isinstance(http_options, dict): - if "headers" in http_options: - http_options["headers"].update(client_header) - else: - http_options["headers"] = client_header - elif hasattr(http_options, "headers"): - if http_options.headers is None: - http_options.headers = client_header - else: - http_options.headers.update(client_header) + http_options = update_google_client_http_options(http_options) self._client = genai.Client(api_key=api_key, http_options=http_options) self.set_model_name(self._params.model) diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index f28a4c1b0..e8152472f 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -22,7 +22,7 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field -from pipecat import __version__ +from pipecat.services.google.utils import update_google_client_http_options from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter, GeminiLLMInvocationParams from pipecat.frames.frames import ( AudioRawFrame, @@ -714,22 +714,7 @@ class GoogleLLMService(LLMService): self.set_model_name(model) self._api_key = api_key self._system_instruction = system_instruction - self._http_options = http_options - - # Add client header - client_header = {"x-goog-api-client": f"pipecat/{__version__}"} - if self._http_options is None: - self._http_options = {"headers": client_header} - elif isinstance(self._http_options, dict): - if "headers" in self._http_options: - self._http_options["headers"].update(client_header) - else: - self._http_options["headers"] = client_header - elif hasattr(self._http_options, "headers"): - if self._http_options.headers is None: - self._http_options.headers = client_header - else: - self._http_options.headers.update(client_header) + self._http_options = update_google_client_http_options(http_options) self._settings = { "max_tokens": params.max_tokens, diff --git a/src/pipecat/services/google/utils.py b/src/pipecat/services/google/utils.py new file mode 100644 index 000000000..91d3a8af4 --- /dev/null +++ b/src/pipecat/services/google/utils.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +from typing import Any, Dict, Optional, Union + +from pipecat import __version__ as pipecat_version + + +def update_google_client_http_options(http_options: Optional[Union[Dict[str, Any], Any]]) -> Any: + """Updates http_options with the x-goog-api-client header. + + Args: + http_options: The existing http_options, which can be None, a dictionary, + or an object with a 'headers' attribute. + + Returns: + The updated http_options. + """ + client_header = {"x-goog-api-client": f"pipecat/{pipecat_version}"} + + if http_options is None: + http_options = {"headers": client_header} + elif isinstance(http_options, dict): + # Create a copy to avoid modifying the original dictionary if it's reused elsewhere + http_options = http_options.copy() + if "headers" in http_options: + http_options["headers"].update(client_header) + else: + http_options["headers"] = client_header + elif hasattr(http_options, "headers"): + # We can't easily copy an arbitrary object, so we modify it in place. + # This assumes the object is mutable and it's safe to do so. + if http_options.headers is None: + http_options.headers = client_header + else: + http_options.headers.update(client_header) + + return http_options From 15df3c06e8a5b2128bef432e09ac7e7e7fd28100 Mon Sep 17 00:00:00 2001 From: thorwebdev <5748289+thorwebdev@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:54:43 -0500 Subject: [PATCH 3/6] chore: add test. --- tests/test_google_utils.py | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_google_utils.py diff --git a/tests/test_google_utils.py b/tests/test_google_utils.py new file mode 100644 index 000000000..b8ffa5a7e --- /dev/null +++ b/tests/test_google_utils.py @@ -0,0 +1,60 @@ +import importlib.util +import sys +import unittest +from unittest.mock import MagicMock + +# Mock Pipecat package +sys.modules["pipecat"] = MagicMock() +sys.modules["pipecat"].__version__ = "0.0.0-test" + +# Load the module directly from source +spec = importlib.util.spec_from_file_location( + "pipecat.services.google.utils", "src/pipecat/services/google/utils.py" +) +utils_module = importlib.util.module_from_spec(spec) +sys.modules["pipecat.services.google.utils"] = utils_module +spec.loader.exec_module(utils_module) + +update_google_client_http_options = utils_module.update_google_client_http_options +pipecat_version = "0.0.0-test" + + +class TestGoogleUtils(unittest.TestCase): + def test_update_google_client_http_options_none(self): + options = update_google_client_http_options(None) + self.assertEqual(options, {"headers": {"x-goog-api-client": f"pipecat/{pipecat_version}"}}) + + def test_update_google_client_http_options_dict_empty(self): + options = update_google_client_http_options({}) + self.assertEqual(options, {"headers": {"x-goog-api-client": f"pipecat/{pipecat_version}"}}) + + def test_update_google_client_http_options_dict_existing_headers(self): + initial_options = {"headers": {"Authorization": "Bearer token"}} + options = update_google_client_http_options(initial_options) + self.assertEqual(options["headers"]["Authorization"], "Bearer token") + self.assertEqual(options["headers"]["x-goog-api-client"], f"pipecat/{pipecat_version}") + + def test_update_google_client_http_options_object(self): + class HttpOptions: + def __init__(self): + self.headers = None + + http_options = HttpOptions() + updated_options = update_google_client_http_options(http_options) + self.assertEqual( + updated_options.headers, {"x-goog-api-client": f"pipecat/{pipecat_version}"} + ) + + def test_update_google_client_http_options_object_existing_headers(self): + class HttpOptions: + def __init__(self): + self.headers = {"Authorization": "Bearer token"} + + http_options = HttpOptions() + updated_options = update_google_client_http_options(http_options) + self.assertEqual(updated_options.headers["Authorization"], "Bearer token") + self.assertEqual(updated_options.headers["x-goog-api-client"], f"pipecat/{pipecat_version}") + + +if __name__ == "__main__": + unittest.main() From b0f63c3785c42b50326262c0d32cfa98a3ac9db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Mon, 8 Dec 2025 11:42:30 -0800 Subject: [PATCH 4/6] pipecat: add version() function --- src/pipecat/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pipecat/__init__.py b/src/pipecat/__init__.py index 1975e1654..d1529a45b 100644 --- a/src/pipecat/__init__.py +++ b/src/pipecat/__init__.py @@ -5,14 +5,20 @@ # import sys -from importlib.metadata import version +from importlib.metadata import version as lib_version from loguru import logger -__version__ = version("pipecat-ai") +__version__ = lib_version("pipecat-ai") logger.info(f"ᓚᘏᗢ Pipecat {__version__} (Python {sys.version}) ᓚᘏᗢ") + +def version() -> str: + """Returns the Pipecat version.""" + return __version__ + + # We replace `asyncio.wait_for()` for `wait_for2.wait_for()` for Python < 3.12. # # In Python 3.12, `asyncio.wait_for()` is implemented in terms of From d289b38ba74ff7bae5126b158b135eccc51b234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Mon, 8 Dec 2025 11:44:45 -0800 Subject: [PATCH 5/6] tests(google): mock the new pipecat.version() --- .../services/google/gemini_live/llm.py | 2 +- src/pipecat/services/google/image.py | 5 ++- src/pipecat/services/google/llm.py | 2 +- src/pipecat/services/google/utils.py | 6 ++-- tests/test_google_utils.py | 35 ++++++++----------- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index cb1ca7d1e..ec608c311 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -24,7 +24,6 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field -from pipecat.services.google.utils import update_google_client_http_options from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.frames.frames import ( @@ -69,6 +68,7 @@ from pipecat.processors.aggregators.openai_llm_context import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.google.frames import LLMSearchOrigin, LLMSearchResponseFrame, LLMSearchResult +from pipecat.services.google.utils import update_google_client_http_options from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.openai.llm import ( OpenAIAssistantContextAggregator, diff --git a/src/pipecat/services/google/image.py b/src/pipecat/services/google/image.py index 9b4e1c615..b1f2ed716 100644 --- a/src/pipecat/services/google/image.py +++ b/src/pipecat/services/google/image.py @@ -22,9 +22,8 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field - -from pipecat.services.google.utils import update_google_client_http_options from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame +from pipecat.services.google.utils import update_google_client_http_options from pipecat.services.image_service import ImageGenService try: @@ -78,7 +77,7 @@ class GoogleImageGenService(ImageGenService): # Add client header http_options = update_google_client_http_options(http_options) - + self._client = genai.Client(api_key=api_key, http_options=http_options) self.set_model_name(self._params.model) diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index e8152472f..f7455c4a4 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -22,7 +22,6 @@ from loguru import logger from PIL import Image from pydantic import BaseModel, Field -from pipecat.services.google.utils import update_google_client_http_options from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter, GeminiLLMInvocationParams from pipecat.frames.frames import ( AudioRawFrame, @@ -51,6 +50,7 @@ from pipecat.processors.aggregators.openai_llm_context import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.google.frames import LLMSearchResponseFrame +from pipecat.services.google.utils import update_google_client_http_options from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.openai.llm import ( OpenAIAssistantContextAggregator, diff --git a/src/pipecat/services/google/utils.py b/src/pipecat/services/google/utils.py index 91d3a8af4..4543468ac 100644 --- a/src/pipecat/services/google/utils.py +++ b/src/pipecat/services/google/utils.py @@ -4,9 +4,11 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Utility functions for Google services.""" + from typing import Any, Dict, Optional, Union -from pipecat import __version__ as pipecat_version +from pipecat import version as pipecat_version def update_google_client_http_options(http_options: Optional[Union[Dict[str, Any], Any]]) -> Any: @@ -19,7 +21,7 @@ def update_google_client_http_options(http_options: Optional[Union[Dict[str, Any Returns: The updated http_options. """ - client_header = {"x-goog-api-client": f"pipecat/{pipecat_version}"} + client_header = {"x-goog-api-client": f"pipecat/{pipecat_version()}"} if http_options is None: http_options = {"headers": client_header} diff --git a/tests/test_google_utils.py b/tests/test_google_utils.py index b8ffa5a7e..940408e3b 100644 --- a/tests/test_google_utils.py +++ b/tests/test_google_utils.py @@ -1,38 +1,33 @@ -import importlib.util -import sys +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import unittest -from unittest.mock import MagicMock -# Mock Pipecat package -sys.modules["pipecat"] = MagicMock() -sys.modules["pipecat"].__version__ = "0.0.0-test" +import pipecat.services.google.utils +from pipecat.services.google.utils import update_google_client_http_options -# Load the module directly from source -spec = importlib.util.spec_from_file_location( - "pipecat.services.google.utils", "src/pipecat/services/google/utils.py" -) -utils_module = importlib.util.module_from_spec(spec) -sys.modules["pipecat.services.google.utils"] = utils_module -spec.loader.exec_module(utils_module) +MOCKED_VERSION = "0.0.0-test" -update_google_client_http_options = utils_module.update_google_client_http_options -pipecat_version = "0.0.0-test" +pipecat.services.google.utils.pipecat_version = lambda: MOCKED_VERSION class TestGoogleUtils(unittest.TestCase): def test_update_google_client_http_options_none(self): options = update_google_client_http_options(None) - self.assertEqual(options, {"headers": {"x-goog-api-client": f"pipecat/{pipecat_version}"}}) + self.assertEqual(options, {"headers": {"x-goog-api-client": f"pipecat/{MOCKED_VERSION}"}}) def test_update_google_client_http_options_dict_empty(self): options = update_google_client_http_options({}) - self.assertEqual(options, {"headers": {"x-goog-api-client": f"pipecat/{pipecat_version}"}}) + self.assertEqual(options, {"headers": {"x-goog-api-client": f"pipecat/{MOCKED_VERSION}"}}) def test_update_google_client_http_options_dict_existing_headers(self): initial_options = {"headers": {"Authorization": "Bearer token"}} options = update_google_client_http_options(initial_options) self.assertEqual(options["headers"]["Authorization"], "Bearer token") - self.assertEqual(options["headers"]["x-goog-api-client"], f"pipecat/{pipecat_version}") + self.assertEqual(options["headers"]["x-goog-api-client"], f"pipecat/{MOCKED_VERSION}") def test_update_google_client_http_options_object(self): class HttpOptions: @@ -42,7 +37,7 @@ class TestGoogleUtils(unittest.TestCase): http_options = HttpOptions() updated_options = update_google_client_http_options(http_options) self.assertEqual( - updated_options.headers, {"x-goog-api-client": f"pipecat/{pipecat_version}"} + updated_options.headers, {"x-goog-api-client": f"pipecat/{MOCKED_VERSION}"} ) def test_update_google_client_http_options_object_existing_headers(self): @@ -53,7 +48,7 @@ class TestGoogleUtils(unittest.TestCase): http_options = HttpOptions() updated_options = update_google_client_http_options(http_options) self.assertEqual(updated_options.headers["Authorization"], "Bearer token") - self.assertEqual(updated_options.headers["x-goog-api-client"], f"pipecat/{pipecat_version}") + self.assertEqual(updated_options.headers["x-goog-api-client"], f"pipecat/{MOCKED_VERSION}") if __name__ == "__main__": From ee435b6f1e8fb517fa2d24119ebb5c902e2697ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Mon, 8 Dec 2025 11:53:00 -0800 Subject: [PATCH 6/6] update CHANGELOG --- changelog/3208.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3208.added.md diff --git a/changelog/3208.added.md b/changelog/3208.added.md new file mode 100644 index 000000000..0cda1dafb --- /dev/null +++ b/changelog/3208.added.md @@ -0,0 +1 @@ +- Added `x-goog-api-client` header with Pipecat's version to all Google services' requests.