Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions python/packages/gemini/agent_framework_gemini/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
detect_media_type_from_base64,
validate_tool_mode,
)
from agent_framework._settings import SecretString, load_settings
from agent_framework._settings import SecretString
from agent_framework._telemetry import mark_feature_used
from agent_framework._types import _get_data_bytes # type: ignore[reportPrivateUsage]
from agent_framework.exceptions import (
Expand All @@ -51,6 +51,7 @@
from ._sdk_client import (
GoogleGeminiSettings,
create_genai_client,
load_gemini_settings,
)

if sys.version_info >= (3, 13):
Expand Down Expand Up @@ -303,7 +304,10 @@ def __init__(
Application Default Credentials.
env_file_path: Path to a ``.env`` file for credential loading.
env_file_encoding: Encoding for the ``.env`` file.
client: Pre-built ``genai.Client`` instance. When provided, connector auth settings are not required.
client: Pre-built ``genai.Client`` instance. When provided, it is used as-is: the API key,
project, and location arguments and connection environment settings are not read.
``GOOGLE_MODEL`` still applies. Explicit mode arguments only apply when the client does not
expose its own mode.
additional_properties: Extra properties stored on the client instance.
max_tracked_thought_signatures: Maximum number of Gemini 3 thought signatures retained
for replay, keyed by call ID. Least-recently-used entries are evicted beyond this.
Expand All @@ -313,9 +317,8 @@ def __init__(
"""
if max_tracked_thought_signatures < 1:
raise ValueError("max_tracked_thought_signatures must be greater than 0.")
google_settings = load_settings(
GoogleGeminiSettings,
env_prefix="GOOGLE_",
google_settings = load_gemini_settings(
client=client,
api_key=api_key,
model=model,
genai_use_enterprise=enterprise,
Expand Down Expand Up @@ -1393,7 +1396,10 @@ def __init__(
Application Default Credentials.
env_file_path: Path to a ``.env`` file for credential loading.
env_file_encoding: Encoding for the ``.env`` file.
client: Pre-built ``genai.Client`` instance. When provided, connector auth settings are not required.
client: Pre-built ``genai.Client`` instance. When provided, it is used as-is: the API key,
project, and location arguments and connection environment settings are not read.
``GOOGLE_MODEL`` still applies. Explicit mode arguments only apply when the client does not
expose its own mode.
additional_properties: Extra properties stored on the client instance.
middleware: Optional middleware chain applied to every call.
function_invocation_configuration: Optional configuration for the function invocation loop.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
EmbeddingGenerationOptions,
GeneratedEmbeddings,
UsageDetails,
load_settings,
)
from agent_framework._settings import SecretString
from agent_framework._telemetry import mark_feature_used
Expand All @@ -32,8 +31,8 @@

from ._feature_usage import FeatureIndex
from ._sdk_client import (
GoogleGeminiSettings,
create_genai_client,
load_gemini_settings,
)

if sys.version_info >= (3, 13):
Expand Down Expand Up @@ -137,7 +136,10 @@ class RawGeminiEmbeddingClient(
project: Enterprise (Vertex AI) project, or ``GOOGLE_CLOUD_PROJECT``.
location: Enterprise (Vertex AI) region, or ``GOOGLE_CLOUD_LOCATION``.
credentials: Google Cloud credentials for Enterprise; the SDK can also use ADC.
client: Preconfigured ``genai.Client``; the caller retains ownership of it.
client: Preconfigured ``genai.Client``; the caller retains ownership of it. When provided, the
API key, project, and location arguments and connection environment settings are not read.
``GOOGLE_EMBEDDING_MODEL`` still applies. Explicit mode arguments only apply when the client
does not expose its own mode.
additional_properties: Extra properties stored on the client instance.
env_file_path: Optional ``.env`` file for settings.
env_file_encoding: Encoding for the ``.env`` file.
Expand All @@ -161,9 +163,8 @@ def __init__(
env_file_encoding: str | None = None,
) -> None:
"""Initialize a raw Gemini embedding client."""
google_settings = load_settings(
GoogleGeminiSettings,
env_prefix="GOOGLE_",
google_settings = load_gemini_settings(
client=client,
api_key=api_key,
embedding_model=model,
genai_use_enterprise=enterprise,
Expand Down
30 changes: 29 additions & 1 deletion python/packages/gemini/agent_framework_gemini/_sdk_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Any

from agent_framework._settings import SecretString
from agent_framework._settings import SecretString, load_settings
from agent_framework._telemetry import get_user_agent
from google import genai
from google.auth.credentials import Credentials
Expand All @@ -23,6 +23,34 @@ class GoogleGeminiSettings(TypedDict, total=False):
cloud_location: str | None


class _GoogleGeminiModelSettings(TypedDict, total=False):
"""The only connector settings an injected ``genai.Client`` still needs."""

model: str | None
embedding_model: str | None


def load_gemini_settings(*, client: genai.Client | None, **kwargs: Any) -> GoogleGeminiSettings:
"""Load ``GOOGLE_*`` settings, skipping auth, mode, project, and location for an injected client.

For an injected client, explicit mode arguments are kept (environment mode is not read) so they
remain the fallback when the SDK client does not expose its mode.
"""
if client is None:
return load_settings(GoogleGeminiSettings, env_prefix="GOOGLE_", **kwargs)
settings: GoogleGeminiSettings = load_settings(_GoogleGeminiModelSettings, env_prefix="GOOGLE_", **kwargs) # type: ignore[assignment]
for key in ("genai_use_enterprise", "genai_use_vertexai"):
if (value := kwargs.get(key)) is not None:
# Match the override type check an owned client gets from load_settings.
if not isinstance(value, bool):
raise ValueError(
f"Invalid type for setting '{key}' from explicit override: "
f"expected bool, got {type(value).__name__}."
)
settings[key] = value
return settings


_GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com"
_VERTEX_AI_BASE_URL = "https://aiplatform.googleapis.com"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,56 @@ def test_injected_vertex_client_controls_service_url() -> None:
assert client.service_url() == "https://aiplatform.googleapis.com"


@pytest.mark.parametrize("env_var", ["GOOGLE_GENAI_USE_ENTERPRISE", "GOOGLE_GENAI_USE_VERTEXAI"])
def test_injected_sdk_client_ignores_invalid_connection_settings(
clear_google_env: None,
env_var: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An injected SDK client is used as-is, so connection settings it does not need are never parsed."""
monkeypatch.setenv(env_var, "not-a-bool")
monkeypatch.setenv("GOOGLE_EMBEDDING_MODEL", "gemini-embedding-2-preview")
sdk = _make_sdk_client()

client = RawGeminiEmbeddingClient(client=sdk)

assert client._genai_client is sdk
assert client._vertexai is False
assert client.service_url() == "https://generativelanguage.googleapis.com"
assert client.model == "gemini-embedding-2-preview"


@pytest.mark.parametrize("mode_arg", ["enterprise", "vertexai"])
def test_injected_sdk_client_without_mode_uses_explicit_mode(
clear_google_env: None,
mode_arg: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An explicit mode stays the fallback when the injected SDK client does not expose its mode."""
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "not-a-bool")
sdk = _make_sdk_client()
sdk._api_client.vertexai = None

client = RawGeminiEmbeddingClient(
client=sdk, model="gemini-embedding-2-preview", **cast(dict[str, Any], {mode_arg: True})
)

assert client._genai_client is sdk
assert client._vertexai is True


@pytest.mark.parametrize("mode_arg", ["enterprise", "vertexai"])
def test_injected_sdk_client_rejects_non_bool_explicit_mode(mode_arg: str) -> None:
"""Explicit mode arguments are type-checked for an injected client, like for an owned one."""
sdk = MagicMock()
sdk._api_client.vertexai = None

with pytest.raises(ValueError, match=f"genai_use_{mode_arg}"):
RawGeminiEmbeddingClient(
client=sdk, model="gemini-embedding-2-preview", **cast(dict[str, Any], {mode_arg: "false"})
)


async def test_close_only_closes_owned_sdk_client(clear_google_env: None) -> None:
owned_sdk = _make_sdk_client()
with patch("agent_framework_gemini._sdk_client.genai.Client", return_value=owned_sdk):
Expand Down
60 changes: 60 additions & 0 deletions python/packages/gemini/tests/test_gemini_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2999,6 +2999,66 @@ def test_injected_sdk_routing_is_shared_with_embeddings() -> None:
assert chat.service_url() == embeddings.service_url() == "https://custom.example.test"


@pytest.mark.parametrize("client_type", [RawGeminiChatClient, GeminiChatClient])
@pytest.mark.parametrize("env_var", ["GOOGLE_GENAI_USE_ENTERPRISE", "GOOGLE_GENAI_USE_VERTEXAI"])
def test_injected_sdk_client_ignores_invalid_connection_settings(
client_type: type[RawGeminiChatClient],
env_var: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An injected SDK client is used as-is, so connection settings it does not need are never parsed."""
monkeypatch.setenv(env_var, "not-a-bool")
monkeypatch.setenv("GOOGLE_MODEL", "env-model")
sdk = MagicMock()
sdk._api_client.vertexai = False
sdk._api_client._http_options.base_url = "https://generativelanguage.googleapis.com/"

client = client_type(client=sdk)

assert client._genai_client is sdk
assert client._vertexai is False
assert client.service_url() == "https://generativelanguage.googleapis.com"
assert client.model == "env-model"


@pytest.mark.parametrize("client_type", [RawGeminiChatClient, GeminiChatClient])
@pytest.mark.parametrize("mode_arg", ["enterprise", "vertexai"])
def test_injected_sdk_client_without_mode_uses_explicit_mode(
client_type: type[RawGeminiChatClient],
mode_arg: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An explicit mode stays the fallback when the injected SDK client does not expose its mode."""
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "not-a-bool")
sdk = MagicMock()
sdk._api_client.vertexai = None
sdk._api_client._http_options.base_url = None

client = client_type(client=sdk, model="gemini-2.5-flash", **cast(dict[str, Any], {mode_arg: True}))

assert client._genai_client is sdk
assert client._vertexai is True
assert client.service_url() == "https://aiplatform.googleapis.com"


@pytest.mark.parametrize("mode_arg", ["enterprise", "vertexai"])
def test_injected_sdk_client_rejects_non_bool_explicit_mode(mode_arg: str) -> None:
"""Explicit mode arguments are type-checked for an injected client, like for an owned one."""
sdk = MagicMock()
sdk._api_client.vertexai = None

with pytest.raises(ValueError, match=f"genai_use_{mode_arg}"):
RawGeminiChatClient(client=sdk, model="gemini-2.5-flash", **cast(dict[str, Any], {mode_arg: "false"}))


def test_owned_client_still_validates_connection_settings(monkeypatch: pytest.MonkeyPatch) -> None:
"""Without an injected SDK client, invalid connection settings still fail construction."""
monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "not-a-bool")

with pytest.raises(ValueError, match="GOOGLE_GENAI_USE_ENTERPRISE"):
GeminiChatClient(api_key="test-key", model="gemini-2.5-flash")


# integration tests


Expand Down
Loading