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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ mistral = [
anthropic = []

gemini = [
"google-genai",
"google-genai>=1.51.0",
"google-cloud-storage",
]

vertexai = [
"google-genai",
"google-genai>=1.51.0",
"google-cloud-storage",
]

Expand Down
24 changes: 24 additions & 0 deletions src/any_llm/providers/gemini/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import re
from typing import TYPE_CHECKING, Any, ClassVar, cast

from typing_extensions import override
Expand Down Expand Up @@ -68,7 +69,26 @@
"xhigh": 32768,
"max": 32768,
}
REASONING_EFFORT_TO_THINKING_LEVELS = {
"minimal": types.ThinkingLevel.MINIMAL,
"low": types.ThinkingLevel.LOW,
"medium": types.ThinkingLevel.MEDIUM,
"high": types.ThinkingLevel.HIGH,
"xhigh": types.ThinkingLevel.HIGH,
"max": types.ThinkingLevel.HIGH,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
_SUPPORTED_BATCH_ENDPOINTS = frozenset({"/v1/chat/completions"})
_THINKING_LEVEL_MIN_GEMINI_VERSION = (3, 5)
_GEMINI_VERSION_PATTERN = re.compile(r"(?:^|/)gemini-(\d+)(?:\.(\d+))?")


def _uses_thinking_level(model_id: str) -> bool:
"""Gemini 3.5 and newer reject `thinking_budget` and expect `thinking_level` instead."""
match = _GEMINI_VERSION_PATTERN.search(model_id.lower())
if match is None:
return False
major, minor = int(match.group(1)), int(match.group(2) or 0)
return (major, minor) >= _THINKING_LEVEL_MIN_GEMINI_VERSION


class GoogleProvider(AnyLLM):
Expand Down Expand Up @@ -139,6 +159,10 @@ def _convert_completion_params(params: CompletionParams, **kwargs: Any) -> dict[
if params.reasoning_effort != "auto":
if params.reasoning_effort is None or params.reasoning_effort == "none":
kwargs["thinking_config"] = types.ThinkingConfig(include_thoughts=False)
elif _uses_thinking_level(params.model_id):
kwargs["thinking_config"] = types.ThinkingConfig(
include_thoughts=True, thinking_level=REASONING_EFFORT_TO_THINKING_LEVELS[params.reasoning_effort]
)
else:
kwargs["thinking_config"] = types.ThinkingConfig(
include_thoughts=True, thinking_budget=REASONING_EFFORT_TO_THINKING_BUDGETS[params.reasoning_effort]
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/providers/test_gemini_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,52 @@ async def test_completion_with_custom_reasoning_effort(reasoning_effort: Reasoni
)


@pytest.mark.parametrize(
("model_id", "reasoning_effort", "expected_level"),
[
("gemini-3.5-flash", "xhigh", types.ThinkingLevel.HIGH),
("gemini-3.5-flash", "max", types.ThinkingLevel.HIGH),
("gemini-3.5-pro", "low", types.ThinkingLevel.LOW),
("models/gemini-3.5-flash", "medium", types.ThinkingLevel.MEDIUM),
("gemini-3.10-flash", "minimal", types.ThinkingLevel.MINIMAL),
("gemini-4-pro", "high", types.ThinkingLevel.HIGH),
],
)
def test_new_gemini_models_use_thinking_level(
model_id: str, reasoning_effort: ReasoningEffort, expected_level: types.ThinkingLevel
) -> None:
result = GoogleProvider._convert_completion_params(
CompletionParams(
model_id=model_id, messages=[{"role": "user", "content": "Hello"}], reasoning_effort=reasoning_effort
),
provider_name="gemini",
)

assert result["config"].thinking_config == types.ThinkingConfig(
include_thoughts=True, thinking_level=expected_level
)


@pytest.mark.parametrize(
"model_id",
[
"gemini-3.0-flash",
"gemini-3.4-flash",
"gemini-3-pro-preview",
"gemini-2.5-flash",
"gemini-pro",
"projects/p/locations/l/publishers/google/models/gemini-3-pro",
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
def test_older_gemini_models_keep_thinking_budget(model_id: str) -> None:
result = GoogleProvider._convert_completion_params(
CompletionParams(model_id=model_id, messages=[{"role": "user", "content": "Hello"}], reasoning_effort="high"),
provider_name="gemini",
)

assert result["config"].thinking_config == types.ThinkingConfig(include_thoughts=True, thinking_budget=24576)


@pytest.mark.asyncio
async def test_completion_with_max_tokens_conversion() -> None:
"""Test that max_tokens parameter gets converted to max_output_tokens."""
Expand Down
Loading