diff --git a/lib/crewai/src/crewai/llms/providers/gemini/completion.py b/lib/crewai/src/crewai/llms/providers/gemini/completion.py index b099fe2373..122f31ad7f 100644 --- a/lib/crewai/src/crewai/llms/providers/gemini/completion.py +++ b/lib/crewai/src/crewai/llms/providers/gemini/completion.py @@ -9,6 +9,7 @@ from pydantic import BaseModel, Field, PrivateAttr, model_validator +from crewai import __version__ from crewai.events.types.llm_events import LLMCallType from crewai.llms.base_llm import BaseLLM, llm_call_context from crewai.llms.hooks.base import BaseInterceptor @@ -181,11 +182,24 @@ def _initialize_client(self, use_vertexai: bool = False) -> genai.Client: When vertexai=True is set, it routes to aiplatform.googleapis.com which rejects API keys. Use Gemini API endpoint for API key authentication instead. """ - client_params = {} + client_params: dict[str, Any] = {} if self.client_params: client_params.update(self.client_params) + http_options = client_params.get("http_options") + if http_options is None: + http_options = types.HttpOptions() + elif isinstance(http_options, dict): + http_options = types.HttpOptions(**http_options) + + if not http_options.headers: + http_options.headers = {"user-agent": f"crewai/{__version__}"} + elif "user-agent" not in http_options.headers: + http_options.headers["user-agent"] = f"crewai/{__version__}" + + client_params["http_options"] = http_options + has_api_key = bool(self.api_key) has_project = bool(self.project) @@ -215,7 +229,7 @@ def _initialize_client(self, use_vertexai: bool = False) -> genai.Client: # See: https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstart?usertype=apikey if use_vertexai: client_params["vertexai"] = True - client_params["http_options"] = types.HttpOptions(api_version="v1") + client_params["http_options"].api_version = "v1" else: # This ensures we use the Gemini API (generativelanguage.googleapis.com) client_params["vertexai"] = False diff --git a/lib/crewai/tests/llms/google/test_google.py b/lib/crewai/tests/llms/google/test_google.py index 2c03dcd23b..6b5005abf7 100644 --- a/lib/crewai/tests/llms/google/test_google.py +++ b/lib/crewai/tests/llms/google/test_google.py @@ -122,6 +122,67 @@ def test_gemini_completion_initialization_parameters(): assert llm.top_k == 40 +def test_gemini_user_agent_header_default(): + """Test that default Gemini client initialization sets the user-agent header.""" + from crewai import __version__ + from crewai.llms.providers.gemini.completion import GeminiCompletion + + llm = GeminiCompletion(model="gemini-3.7-flash", api_key="test-key") + client = llm._get_sync_client() + assert client._api_client._http_options is not None + assert client._api_client._http_options.headers is not None + assert f"crewai/{__version__}" in client._api_client._http_options.headers["user-agent"] + + +def test_gemini_http_options_dict_conversion(): + """Test that dictionary http_options in client_params are converted to types.HttpOptions.""" + from crewai import __version__ + from crewai.llms.providers.gemini.completion import GeminiCompletion + + llm = GeminiCompletion( + model="gemini-3.7-flash", + api_key="test-key", + client_params={"http_options": {"timeout": 45}}, + ) + client = llm._get_sync_client() + assert client._api_client._http_options is not None + assert client._api_client._http_options.timeout == 45 + assert f"crewai/{__version__}" in client._api_client._http_options.headers["user-agent"] + + +def test_gemini_http_options_custom_headers_preserved(): + """Test that custom headers in types.HttpOptions are preserved alongside user-agent.""" + from google.genai import types + from crewai import __version__ + from crewai.llms.providers.gemini.completion import GeminiCompletion + + llm = GeminiCompletion( + model="gemini-3.7-flash", + api_key="test-key", + client_params={"http_options": types.HttpOptions(headers={"X-Custom-Header": "my-value"})}, + ) + client = llm._get_sync_client() + assert client._api_client._http_options is not None + assert client._api_client._http_options.headers.get("X-Custom-Header") == "my-value" + assert f"crewai/{__version__}" in client._api_client._http_options.headers["user-agent"] + + +def test_gemini_vertex_express_mode_preserves_user_agent(): + """Test that Vertex AI Express mode sets api_version='v1' and preserves user-agent.""" + from crewai import __version__ + from crewai.llms.providers.gemini.completion import GeminiCompletion + + llm = GeminiCompletion( + model="gemini-3.7-flash", + api_key="test-key", + use_vertexai=True, + ) + client = llm._get_sync_client() + assert client._api_client._http_options is not None + assert client._api_client._http_options.api_version == "v1" + assert f"crewai/{__version__}" in client._api_client._http_options.headers["user-agent"] + + def test_gemini_started_event_surfaces_max_output_tokens(): from crewai.events.event_bus import crewai_event_bus from crewai.events.types.llm_events import LLMCallStartedEvent