This commit is contained in:
Seongho Bae 2026-11-06 05:02:43 +00:00 • committed by GitHub
commit 625bf7ae7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 290 additions and 6 deletions

View file

@ -459,6 +459,7 @@ class GoogleGenAIAdapter:
contents: _JsonDictList | _JsonDict,
config: Mapping[str, object] | None = None,
litellm_params: GenericLiteLLMParams | None = None,
reasoning_effort: object = None,
**kwargs,
) -> dict[str, Any]:
"""
@ -545,14 +546,38 @@ class GoogleGenAIAdapter:
#########################################################
# forward any litellm specific params
#########################################################
completion_request_dict = dict(completion_request)
if litellm_params:
completion_request_dict = self._add_generic_litellm_params_to_request(
direct_effort: Final = reasoning_effort
generic_effort: Final[object] = getattr(litellm_params, "reasoning_effort", None)
direct_effort_fields: Final = _validated(_CONFIG_FIELDS, direct_effort)
generic_effort_fields: Final = _validated(_CONFIG_FIELDS, generic_effort)
if (
direct_effort is not None
and generic_effort is not None
and (
type(direct_effort) is not type(generic_effort)
or direct_effort != generic_effort
or (
isinstance(direct_effort, dict)
and isinstance(generic_effort, dict)
and direct_effort_fields is not None
and generic_effort_fields is not None
and type(direct_effort_fields.get("effort")) is not type(generic_effort_fields.get("effort"))
)
)
):
raise ValueError("Conflicting reasoning_effort in kwargs and litellm_params")
completion_request_dict: Final = {
**completion_request,
**({"reasoning_effort": reasoning_effort} if reasoning_effort is not None else {}),
}
return (
self._add_generic_litellm_params_to_request(
completion_request_dict=completion_request_dict,
litellm_params=litellm_params,
)
return completion_request_dict
if litellm_params
else completion_request_dict
)
def _add_generic_litellm_params_to_request(
self,
@ -574,6 +599,9 @@ class GoogleGenAIAdapter:
for key, value in litellm_dict.items():
if key in allowed_fields:
completion_request_dict[key] = value
reasoning_effort: Final[object] = getattr(litellm_params, "reasoning_effort", None)
if reasoning_effort is not None:
return {**completion_request_dict, "reasoning_effort": reasoning_effort}
return completion_request_dict
def translate_completion_output_params_streaming(

View file

@ -5,14 +5,270 @@ Test to verify the Google GenAI generate_content adapter functionality
import json
import unittest
from typing import Final
from copy import deepcopy
from typing import Final, Literal
import httpx
import pytest
import respx
import litellm
import litellm.google_genai.main as google_genai_main
from litellm.google_genai.adapters.handler import GenerateContentToCompletionHandler
from litellm.google_genai.adapters.transformation import GoogleGenAIAdapter
from litellm.google_genai.main import agenerate_content
from litellm.llms.vertex_ai.gemini.transformation import gemini_convert_messages_with_history
from litellm.types.router import GenericLiteLLMParams
@pytest.mark.parametrize("carrier", ["generic", "kwargs"])
@pytest.mark.parametrize("effort", ["high", {"effort": "high"}], ids=["string", "object"])
def test_explicit_reasoning_effort_survives_completion_adapter(
carrier: Literal["generic", "kwargs"], effort: str | dict[str, str]
) -> None:
params: Final = GenericLiteLLMParams(**({"reasoning_effort": effort} if carrier == "generic" else {}))
extra_kwargs: Final = {"reasoning_effort": effort} if carrier == "kwargs" else {}
result: Final = GenerateContentToCompletionHandler._prepare_completion_kwargs(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
config={"temperature": 0.0},
litellm_params=params,
extra_kwargs=extra_kwargs,
)
assert result["reasoning_effort"] == effort
assert type(result["reasoning_effort"]) is type(effort)
assert result["temperature"] == 0.0
@pytest.mark.parametrize(
("entrypoint", "is_async", "stream"),
[
("generate_content", False, False),
("agenerate_content", True, False),
("generate_content_stream", False, True),
("agenerate_content_stream", True, True),
],
)
@pytest.mark.parametrize(
"effort",
["high", {"effort": "high"}, {"effort": "high", "summary": "detailed"}, None],
ids=["string", "object", "summary", "null"],
)
@pytest.mark.asyncio
async def test_public_generate_content_preserves_explicit_reasoning_effort(
monkeypatch: pytest.MonkeyPatch, entrypoint: str, is_async: bool, stream: bool, effort: str | dict[str, str] | None
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
config: Final = {"temperature": 0.0}
contents: Final = [{"role": "user", "parts": [{"text": "Hello"}]}]
before: Final = deepcopy((contents, config, effort))
upstream_body: Final = {
"id": "chatcmpl-effort",
"object": "chat.completion",
"created": 1234567890,
"model": "adapter-test-model",
"choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "Hello"}}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
upstream_chunk: Final = {
"id": "chatcmpl-effort",
"object": "chat.completion.chunk",
"created": 1234567890,
"model": "adapter-test-model",
"choices": [{"index": 0, "finish_reason": "stop", "delta": {"content": "Hello"}}],
}
with respx.mock() as upstream:
route: Final = upstream.post("https://upstream.test/v1/chat/completions").mock(
return_value=(
httpx.Response(
200,
text=f"data: {json.dumps(upstream_chunk)}\n\ndata: [DONE]\n\n",
headers={"content-type": "text/event-stream"},
)
if stream
else httpx.Response(200, json=upstream_body)
)
)
call: Final = getattr(google_genai_main, entrypoint)
response: Final = call(
model="openai/adapter-test-model",
contents=contents,
config=config,
api_base="https://upstream.test/v1",
api_key="unit-test",
reasoning_effort=effort,
)
result: Final = await response if is_async else response
output: Final = (
tuple([json.loads(chunk.removeprefix(b"data: ").strip()) async for chunk in result])
if stream
else (result,)
)
assert len(route.calls) == 1
assert json.loads(route.calls[0].request.content) == {
"model": "adapter-test-model",
"messages": [{"role": "user", "content": "Hello"}],
"temperature": 0.0,
**({"reasoning_effort": effort} if effort is not None else {}),
**({"stream": True, "stream_options": {"include_usage": True}} if stream else {}),
}
assert output[0]["candidates"][0]["content"]["parts"] == [{"text": "Hello"}]
assert (contents, config, effort) == before
@pytest.mark.parametrize(
"config",
[None, {"temperature": None}, {"temperature": 0.0}, {"temperature": 0.7}, {"temperature": 1.0}],
ids=["missing", "null", "zero", "positive", "one"],
)
@pytest.mark.parametrize("carrier", ["generic", "kwargs"])
@pytest.mark.parametrize("effort", ["high", {"effort": "high", "summary": "detailed"}], ids=["string", "summary"])
def test_explicit_effort_does_not_change_config_or_forward_unrelated_extras(
config: dict[str, object] | None, carrier: Literal["generic", "kwargs"], effort: str | dict[str, str]
) -> None:
supplied: Final = {"reasoning_effort": effort, "unrelated_adapter_extra": "sentinel"}
params: Final = GenericLiteLLMParams(**supplied) if carrier == "generic" else GenericLiteLLMParams()
extras: Final = supplied if carrier == "kwargs" else {}
before: Final = deepcopy((config, params.model_dump(), extras))
result: Final = GenerateContentToCompletionHandler._prepare_completion_kwargs(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
config=config,
litellm_params=params,
extra_kwargs=extras,
)
assert dict(result) == {
"model": "openai/adapter-test-model",
"messages": [{"role": "user", "content": "Hello"}],
**(config or {}),
**GenericLiteLLMParams().model_dump(exclude_none=True),
"reasoning_effort": effort,
}
assert result.get("reasoning_effort") is effort
assert (config, params.model_dump(), extras) == before
@pytest.mark.parametrize(
"config",
[
None,
{"temperature": None},
{"temperature": 0.0},
{"temperature": 0.7},
{"temperature": 1.0},
{"thinkingConfig": {"thinkingLevel": "HIGH", "thinkingBudget": 1024}},
],
ids=["missing", "null", "zero", "positive", "one", "native-thinking"],
)
def test_completion_adapter_does_not_invent_reasoning_effort(config: dict[str, object] | None) -> None:
result: Final = GenerateContentToCompletionHandler._prepare_completion_kwargs(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
config=config,
)
assert dict(result) == {
"model": "openai/adapter-test-model",
"messages": [{"role": "user", "content": "Hello"}],
**({"temperature": config["temperature"]} if config and "temperature" in config else {}),
}
@pytest.mark.parametrize(("direct", "generic"), [(None, "high"), ("high", None), (None, None)])
def test_null_reasoning_effort_does_not_override_an_explicit_value(direct: str | None, generic: str | None) -> None:
result: Final = GenerateContentToCompletionHandler._prepare_completion_kwargs(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params=GenericLiteLLMParams(reasoning_effort=generic),
extra_kwargs={"reasoning_effort": direct},
)
assert dict(result) == {
"model": "openai/adapter-test-model",
"messages": [{"role": "user", "content": "Hello"}],
**GenericLiteLLMParams().model_dump(exclude_none=True),
**({"reasoning_effort": direct or generic} if direct or generic else {}),
}
@pytest.mark.parametrize(
("direct", "generic"),
[
("low", "high"),
({"effort": "low"}, {"effort": "high"}),
("high", {"effort": "high"}),
(False, 0),
({"effort": False}, {"effort": 0}),
],
ids=["strings", "objects", "string-object", "bool-int", "nested-bool-int"],
)
def test_conflicting_reasoning_effort_is_rejected_without_changing_inputs(direct: object, generic: object) -> None:
params: Final = GenericLiteLLMParams(reasoning_effort=generic)
extras: Final = {"reasoning_effort": direct}
before: Final = deepcopy((params.model_dump(), extras))
with pytest.raises(ValueError, match=r"^Conflicting reasoning_effort in kwargs and litellm_params$"):
GenerateContentToCompletionHandler._prepare_completion_kwargs(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params=params,
extra_kwargs=extras,
)
assert (params.model_dump(), extras) == before
@pytest.mark.parametrize("async_mode", [False, True], ids=["sync", "async"])
@pytest.mark.asyncio
async def test_conflicting_reasoning_effort_never_reaches_upstream(
monkeypatch: pytest.MonkeyPatch, async_mode: bool
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
params: Final = GenericLiteLLMParams(
api_base="https://upstream.test/v1",
api_key="unit-test",
reasoning_effort="high",
)
async def conflicting_call() -> None:
response: Final = GenerateContentToCompletionHandler.generate_content_handler(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params=params,
reasoning_effort="low",
_is_async=async_mode,
)
if async_mode:
await response
with respx.mock(assert_all_called=False) as upstream:
route: Final = upstream.post("https://upstream.test/v1/chat/completions").mock(return_value=httpx.Response(500))
with pytest.raises(ValueError, match=r"^Conflicting reasoning_effort in kwargs and litellm_params$"):
await conflicting_call()
assert len(route.calls) == 0
@pytest.mark.parametrize("carrier", ["generic", "kwargs"])
@pytest.mark.parametrize("effort", ["", 0, False, {}, {"effort": 0}], ids=["empty", "zero", "false", "object", "bad-effort"])
def test_single_invalid_reasoning_effort_is_left_for_the_completion_validator(
carrier: Literal["generic", "kwargs"], effort: object
) -> None:
result: Final = GenerateContentToCompletionHandler._prepare_completion_kwargs(
model="openai/adapter-test-model",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params=GenericLiteLLMParams(**({"reasoning_effort": effort} if carrier == "generic" else {})),
extra_kwargs={"reasoning_effort": effort} if carrier == "kwargs" else {},
)
assert result.get("reasoning_effort") is effort
@pytest.mark.parametrize("generic_api_base", [None, "https://generic.test/v1"])
def test_preserving_reasoning_effort_keeps_declared_generic_merge_precedence(generic_api_base: str | None) -> None:
result: Final = GoogleGenAIAdapter()._add_generic_litellm_params_to_request(
{"api_base": "https://translated.test/v1"},
GenericLiteLLMParams(api_base=generic_api_base, reasoning_effort="high", unrelated_adapter_extra="sentinel"),
)
assert result == {
**GenericLiteLLMParams().model_dump(exclude_none=True),
"api_base": generic_api_base or "https://translated.test/v1",
"reasoning_effort": "high",
}
@pytest.mark.parametrize(("data_key", "mime_key"), [("inline_data", "mime_type"), ("inlineData", "mimeType")])