Skip to content
Open
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ uv add ai
```

AI Gateway API-key usage works with the base package. Direct providers that
use an OpenAI-compatible or Anthropic-compatible adapter load the corresponding
official SDK lazily. Vercel OIDC for AI Gateway also uses an optional extra:
use an OpenAI-compatible, Anthropic-compatible, or Google adapter load the
corresponding official SDK lazily. Vercel OIDC for AI Gateway also uses an
optional extra:

```bash
uv add "ai[openai]" # OpenAI-compatible providers
uv add "ai[anthropic]" # Anthropic-compatible providers
uv add "ai[google]" # Google Gemini
uv add "ai[vercel]" # Vercel OIDC for AI Gateway
```

Expand Down Expand Up @@ -72,12 +74,14 @@ model = ai.get_model("openai/gpt-5.4") # provider omitted: defaults to gateway
model = ai.get_model("gateway:openai/gpt-5.4")
model = ai.get_model("openai:gpt-5.4")
model = ai.get_model("anthropic:claude-sonnet-4-6")
model = ai.get_model("google:gemini-2.5-flash")
```

Provider IDs without a `provider:` prefix route through AI Gateway by default.
Direct OpenAI-compatible providers, including `openai:` and compatible
models.dev provider IDs, require `ai[openai]`. Direct Anthropic-compatible
providers require `ai[anthropic]`.
providers require `ai[anthropic]`. The direct Google provider requires
`ai[google]`.

Structured output:

Expand Down
1 change: 1 addition & 0 deletions docs/ai-python/content/docs/basics/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Providers read their provider-specific keys:
export AI_GATEWAY_API_KEY="your_access_token_here"
export OPENAI_API_KEY="your_access_token_here"
export ANTHROPIC_API_KEY="your_access_token_here"
export GEMINI_API_KEY="your_access_token_here"
```

## Use an explicit provider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: "ai.providers.google"
description: Reference for Google provider APIs.
type: reference
summary: Reference for ai.providers.google.
---

`ai.providers.google` contains the Google Gemini provider, protocol, and
provider-executed tools.

```python
provider = ai.get_provider("google")
model = ai.Model(id="gemini-2.5-flash", provider=provider)
```

The optional upstream `google-genai` SDK loads lazily when the provider
creates or uses an SDK client.

## GoogleProvider

`GoogleProvider` implements `Provider` for the Google Gemini API.

Default configuration for the `google` provider uses:

- `GOOGLE_GENERATIVE_AI_API_KEY`, falling back to `GEMINI_API_KEY` and
`GOOGLE_API_KEY`
- `GOOGLE_GEMINI_BASE_URL`

Pass `base_url`, `api_key`, or a custom client through `get_provider` when you
need explicit configuration.

```python
provider = ai.get_provider(
"google",
base_url="https://gemini.example.com",
)
model = ai.Model(id="gemini-2.5-flash", provider=provider)
```

The provider supports model listing, probing, streaming, provider-executed
tools, and custom `google.genai.Client` instances.

## GoogleGenerateContentProtocol

`GoogleGenerateContentProtocol` translates SDK messages and params to the
Google generateContent API wire format.

The provider uses this protocol by default. Use it directly only when you need
a protocol override.

## Tools

Provider-executed Google tools are documented on the child `tools` page.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "google",
"description": "Reference for Google provider APIs.",
"pages": [
"tools"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: "tools"
description: Reference for Google provider-executed tools.
type: reference
summary: Reference for Google provider tool helpers.
---

Google tool helpers create provider-executed `ai.Tool` declarations.

## Tools

- `google_search`
- `url_context`
- `code_execution`
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ provider-specific namespaces.
- `ProviderProtocol`
- `OpenAICompatibleProvider`
- `AnthropicCompatibleProvider`
- `GoogleProvider`
- `GatewayProvider`
- `history_utils`

Expand Down
3 changes: 2 additions & 1 deletion docs/ai-python/content/docs/reference/ai.providers/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"pages": [
"ai-gateway",
"openai",
"anthropic"
"anthropic",
"google"
]
}
4 changes: 2 additions & 2 deletions docs/ai-python/content/docs/reference/ai/get-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ provider = ai.get_provider(
)
```

Known providers include AI Gateway, OpenAI-compatible providers, and
Anthropic-compatible providers.
Known providers include AI Gateway, OpenAI-compatible providers,
Anthropic-compatible providers, and the Google Gemini provider.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies = [

[project.optional-dependencies]
anthropic = ["anthropic>=0.83.0"]
google = ["google-genai>=2.0.0"]
mcp = ["mcp>=1.18.0"]
openai = ["openai>=2.14.0"]
otel = [
Expand Down Expand Up @@ -65,6 +66,7 @@ bump = true
[dependency-groups]
dev = [
"anthropic>=0.83.0",
"google-genai>=2.0.0",
"mcp>=1.18.0",
"python-dotenv>=1.2.1",
"pytest>=8.0",
Expand Down
2 changes: 2 additions & 0 deletions src/ai/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from .ai_gateway import GatewayProvider
from .anthropic import AnthropicCompatibleProvider
from .base import Provider, ProviderProtocol, get_provider
from .google import GoogleProvider
from .openai import OpenAICompatibleProvider

__all__ = [
"AnthropicCompatibleProvider",
"GatewayProvider",
"GoogleProvider",
"OpenAICompatibleProvider",
"Provider",
"ProviderProtocol",
Expand Down
28 changes: 28 additions & 0 deletions src/ai/providers/google/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Google provider.

Usage::

import ai
from ai.providers.google import tools as google_tools

model = ai.get_model("google:gemini-2.5-flash")
provider = ai.get_provider("google", api_key="...")
model = ai.Model(id="gemini-2.5-flash", provider=provider)
ids = await ai.get_provider("google").list_models()

# built-in tools
async with ai.stream(
model, msgs,
tools=[google_tools.google_search()],
) as s:
...

The optional upstream ``google-genai`` SDK is loaded lazily when the
provider creates or uses an SDK client.
"""

from . import tools
from .protocol import GoogleGenerateContentProtocol
from .provider import GoogleProvider

__all__ = ["GoogleGenerateContentProtocol", "GoogleProvider", "tools"]
43 changes: 43 additions & 0 deletions src/ai/providers/google/_sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Lazy Google GenAI SDK imports."""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, cast

from .. import _optional

if TYPE_CHECKING:
import google.genai as genai
from google.genai import errors as genai_errors


class GoogleSDK(Protocol):
Client: type[genai.Client]


class GoogleErrors(Protocol):
APIError: type[genai_errors.APIError]
ClientError: type[genai_errors.ClientError]
ServerError: type[genai_errors.ServerError]


def import_sdk(*, provider: str = "google") -> GoogleSDK:
return cast(
"GoogleSDK",
_optional.import_optional_sdk(
"google.genai",
provider=provider,
extra="google",
),
)


def import_errors(*, provider: str = "google") -> GoogleErrors:
return cast(
"GoogleErrors",
_optional.import_optional_sdk(
"google.genai.errors",
provider=provider,
extra="google",
),
)
111 changes: 111 additions & 0 deletions src/ai/providers/google/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Google GenAI SDK error mapping."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast

import httpx

from ... import errors as ai_errors

if TYPE_CHECKING:
from google.genai import errors as genai_errors


def map_error(
exc: genai_errors.APIError,
*,
provider: str | None = None,
model_id: str | None = None,
) -> ai_errors.ProviderAPIError:
"""Map a Google GenAI SDK exception to the public provider hierarchy."""
status_code = exc.code if isinstance(exc.code, int) else None
if status_code == 404 and model_id is not None:
cls: type[ai_errors.ProviderAPIError] = (
ai_errors.ProviderModelNotFoundError
)
elif status_code is not None:
cls = ai_errors.http_status_to_provider_status_error_class(status_code)
else:
cls = ai_errors.ProviderAPIError
return _provider_error(cls, exc, provider=provider, model_id=model_id)


def _provider_error(
cls: type[ai_errors.ProviderAPIError],
exc: genai_errors.APIError,
*,
provider: str | None,
model_id: str | None,
) -> ai_errors.ProviderAPIError:
body = exc.details
if issubclass(cls, ai_errors.ProviderModelNotFoundError):
if model_id is None: # pragma: no cover - guarded by map_error
raise RuntimeError(
"model_id is required for ProviderModelNotFoundError"
)
return cls(
_message(exc),
model_id=model_id,
provider=provider,
http_context=_http_context(exc),
body=body,
error_type=exc.status,
)
return cls(
_message(exc),
provider=provider,
http_context=_http_context(exc),
body=body,
error_type=exc.status,
)


def map_httpx_error(
exc: httpx.HTTPError,
*,
provider: str | None = None,
) -> ai_errors.ProviderAPIError:
"""Map a raw httpx transport error to the public provider hierarchy.

The Google GenAI SDK does not wrap transport failures, so connection
and timeout errors surface as bare httpx exceptions.
"""
cls: type[ai_errors.ProviderAPIError] = (
ai_errors.ProviderTimeoutError
if isinstance(exc, httpx.TimeoutException)
else ai_errors.ProviderConnectionError
)
return cls(
str(exc) or type(exc).__name__,
provider=provider,
is_retryable=True,
)


def _http_context(
exc: genai_errors.APIError,
) -> ai_errors.HTTPErrorContext | None:
if not isinstance(exc.code, int):
return None
response = cast("Any", getattr(exc, "response", None))
if not isinstance(response, httpx.Response):
return ai_errors.HTTPErrorContext(status_code=exc.code)
try:
request = response.request
except RuntimeError:
request = None
return ai_errors.HTTPErrorContext(
status_code=exc.code,
request=request,
response=response,
)


def _message(exc: genai_errors.APIError) -> str:
if isinstance(exc.message, str) and exc.message:
return exc.message
return str(exc)


__all__ = ["map_error", "map_httpx_error"]
Loading