Skip to content

Commit 2baa5b3

Browse files
caizifenclaude
andcommitted
feat: Add ThinkingConfig support to GenerationConfig
Expose thinking_level, thinking_budget, and include_thoughts in the high-level vertexai.generative_models.GenerationConfig wrapper by aliasing the v1 GAPIC ThinkingConfig type and converting to v1beta1 via binary serialization to preserve thinking_level through the v1beta1 → v1 round-trip in GA GenerativeModel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ee9fbe1 commit 2baa5b3

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

tests/unit/vertexai/test_generative_models.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,73 @@ def test_generation_config_response_schema_dict_renaming(self):
15371537
)
15381538
assert config.to_dict()["response_schema"] == _RENAMING_EXPECTED_SCHEMA
15391539

1540+
def test_generation_config_with_thinking_budget(self):
1541+
config = generative_models.GenerationConfig(
1542+
thinking_config=generative_models.GenerationConfig.ThinkingConfig(
1543+
thinking_budget=1024,
1544+
),
1545+
)
1546+
config_dict = config.to_dict()
1547+
assert config_dict["thinking_config"]["thinking_budget"] == 1024
1548+
1549+
def test_generation_config_with_include_thoughts(self):
1550+
config = generative_models.GenerationConfig(
1551+
thinking_config=generative_models.GenerationConfig.ThinkingConfig(
1552+
include_thoughts=True,
1553+
),
1554+
)
1555+
config_dict = config.to_dict()
1556+
assert config_dict["thinking_config"]["include_thoughts"] is True
1557+
1558+
def test_generation_config_with_thinking_level(self):
1559+
ThinkingConfig = generative_models.GenerationConfig.ThinkingConfig
1560+
config = generative_models.GenerationConfig(
1561+
thinking_config=ThinkingConfig(
1562+
thinking_level=ThinkingConfig.ThinkingLevel.HIGH,
1563+
),
1564+
)
1565+
# thinking_level is a v1-only field; verify it survives the
1566+
# v1 → v1beta1 → v1 binary serialization round-trip.
1567+
raw = config._raw_generation_config
1568+
serialized = type(raw).serialize(raw)
1569+
deserialized_v1 = types_v1.GenerationConfig.deserialize(serialized)
1570+
assert deserialized_v1.thinking_config.thinking_level == (
1571+
types_v1.GenerationConfig.ThinkingConfig.ThinkingLevel.HIGH
1572+
)
1573+
1574+
def test_generation_config_with_thinking_config_combined(self):
1575+
ThinkingConfig = generative_models.GenerationConfig.ThinkingConfig
1576+
config = generative_models.GenerationConfig(
1577+
thinking_config=ThinkingConfig(
1578+
thinking_budget=4096,
1579+
include_thoughts=True,
1580+
thinking_level=ThinkingConfig.ThinkingLevel.MEDIUM,
1581+
),
1582+
)
1583+
config_dict = config.to_dict()
1584+
assert config_dict["thinking_config"]["thinking_budget"] == 4096
1585+
assert config_dict["thinking_config"]["include_thoughts"] is True
1586+
# Verify thinking_level survives binary round-trip
1587+
raw = config._raw_generation_config
1588+
serialized = type(raw).serialize(raw)
1589+
deserialized_v1 = types_v1.GenerationConfig.deserialize(serialized)
1590+
assert deserialized_v1.thinking_config.thinking_level == (
1591+
types_v1.GenerationConfig.ThinkingConfig.ThinkingLevel.MEDIUM
1592+
)
1593+
1594+
def test_generation_config_thinking_config_from_dict(self):
1595+
config = generative_models.GenerationConfig.from_dict(
1596+
{
1597+
"thinking_config": {
1598+
"thinking_budget": 2048,
1599+
"include_thoughts": True,
1600+
},
1601+
}
1602+
)
1603+
config_dict = config.to_dict()
1604+
assert config_dict["thinking_config"]["thinking_budget"] == 2048
1605+
assert config_dict["thinking_config"]["include_thoughts"] is True
1606+
15401607
def test_tool_schema_dict_renaming(self):
15411608
# The `Tool` constructor does not take a dict so we don't test it here.
15421609
tool = generative_models.Tool.from_dict(

vertexai/generative_models/_generative_models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,6 +1764,7 @@ class GenerationConfig:
17641764

17651765
Modality = gapic_content_types.GenerationConfig.Modality
17661766
ModelConfig = gapic_content_types.GenerationConfig.ModelConfig
1767+
ThinkingConfig = types_v1.GenerationConfig.ThinkingConfig
17671768

17681769
def __init__(
17691770
self,
@@ -1785,6 +1786,7 @@ def __init__(
17851786
response_logprobs: Optional[bool] = None,
17861787
response_modalities: Optional[List["GenerationConfig.Modality"]] = None,
17871788
model_config: Optional["GenerationConfig.ModelConfig"] = None,
1789+
thinking_config: Optional["GenerationConfig.ThinkingConfig"] = None,
17881790
):
17891791
r"""Constructs a GenerationConfig object.
17901792
@@ -1817,6 +1819,11 @@ def __init__(
18171819
logprobs: Logit probabilities.
18181820
reponse_logprobs: If true, export the logprobs results in response.
18191821
model_config: Sets cost vs quality preference for model routing requests.
1822+
thinking_config: Configuration for thinking features
1823+
(thinking_level, thinking_budget, include_thoughts). Use
1824+
``GenerationConfig.ThinkingConfig`` to construct. Note:
1825+
``thinking_level`` is not preserved by ``to_dict()``/``from_dict()``
1826+
due to v1beta1 proto limitations.
18201827
18211828
Usage:
18221829
@@ -1864,6 +1871,15 @@ def __init__(
18641871
self._raw_generation_config.routing_config = (
18651872
routing_config._gapic_routing_config
18661873
)
1874+
if thinking_config is not None:
1875+
# Convert v1 ThinkingConfig to v1beta1 via binary serialization.
1876+
# This preserves thinking_level (field 4) as an unknown field in
1877+
# v1beta1, which survives the v1beta1 → v1 conversion in GA model.
1878+
self._raw_generation_config.thinking_config = (
1879+
gapic_content_types.GenerationConfig.ThinkingConfig.deserialize(
1880+
type(thinking_config).serialize(thinking_config)
1881+
)
1882+
)
18671883

18681884
@classmethod
18691885
def _from_gapic(

0 commit comments

Comments
 (0)