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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ via the OpenAI SDK. It defaults to
`https://dashscope-intl.aliyuncs.com/compatible-mode/v1` and accepts `base_url`
or `DASHSCOPE_BASE_URL` for other regions or workspaces.

The Google provider accepts `top_p`, `top_k`, `stop_sequences`, and `seed` in
both `chat()` and `complete()`. `stop` is an alias for `stop_sequences`; passing
both, or passing any other keyword, raises `TypeError` before
the request is sent. Google finish reasons are returned as lower-case native
values such as `stop`, `max_tokens`, and `safety`; a missing or unspecified
reason is returned as `unknown`.

## The interface

```python
Expand Down
53 changes: 50 additions & 3 deletions src/llm_bridge/providers/google_genai.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,56 @@
from __future__ import annotations

import os
import re
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Mapping, Optional

from llm_bridge.base import LLMClient, LLMResponse, Message, split_system_messages

_GENERATION_ARGUMENTS = ("top_p", "top_k", "stop_sequences", "seed")
_FINISH_REASON_TOKEN = re.compile(r"\A[A-Za-z][A-Za-z0-9_]*\Z")


def _generation_config_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]:
"""Validate and map Google-specific generation arguments."""
unknown = sorted(set(kwargs) - set(_GENERATION_ARGUMENTS) - {"stop"})
if unknown:
raise TypeError(f"Unsupported Google generation arguments: {', '.join(unknown)}")
if "stop" in kwargs and "stop_sequences" in kwargs:
raise TypeError("'stop' and 'stop_sequences' cannot be used together")

config = {name: kwargs[name] for name in _GENERATION_ARGUMENTS if name in kwargs}
if "stop" in kwargs:
config["stop_sequences"] = kwargs["stop"]
return config


def _extract_finish_reason(response: Any) -> str:
"""Return the first lower-case Google finish-reason token."""
try:
candidates = getattr(response, "candidates", None)
if not candidates:
return "unknown"
candidate = candidates[0]
if candidate is None:
return "unknown"
reason = getattr(candidate, "finish_reason", None)
if reason is None:
return "unknown"
try:
value = reason.value
except AttributeError:
value = reason if isinstance(reason, str) else None
except AttributeError:
return "unknown"

if not isinstance(value, str):
return "unknown"
value = value.strip()
if value.upper() == "FINISH_REASON_UNSPECIFIED" or not _FINISH_REASON_TOKEN.fullmatch(value):
return "unknown"
return value.lower()


class GoogleClient(LLMClient):
"""Chat client backed by Google's Gen AI (Gemini) SDK."""
Expand All @@ -28,7 +73,7 @@ def __init__(self, model: str, api_key: Optional[str] = None):

try:
from google import genai
except ImportError as exc: # pragma: no cover
except ImportError as exc:
raise ImportError(
"The 'google' provider requires the google-genai SDK. "
"Install it with: pip install llm-bridge[google]"
Expand All @@ -54,6 +99,7 @@ def chat(
max_tokens: int = 1024,
**kwargs: Any,
) -> LLMResponse:
generation_config = _generation_config_kwargs(kwargs)
from google.genai import types

system_text, turns = split_system_messages(messages)
Expand All @@ -68,6 +114,7 @@ def chat(
temperature=temperature,
max_output_tokens=max_tokens,
system_instruction=system_text,
**generation_config,
)

start = time.perf_counter() * 1000
Expand All @@ -82,7 +129,7 @@ def chat(
model=self._model,
prompt_tokens=getattr(usage, "prompt_token_count", 0) if usage else 0,
completion_tokens=getattr(usage, "candidates_token_count", 0) if usage else 0,
finish_reason="stop",
finish_reason=_extract_finish_reason(resp),
latency_ms=latency,
raw=resp,
)
Expand Down
Loading
Loading