Skip to content

Commit 6431394

Browse files
committed
feat: configure anthropic output_config effort
1 parent 2b72e5f commit 6431394

2 files changed

Lines changed: 402 additions & 18 deletions

File tree

src/google/adk/models/anthropic_llm.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,44 @@
4848
if TYPE_CHECKING:
4949
from .llm_request import LlmRequest
5050

51-
__all__ = ["AnthropicLlm", "Claude"]
51+
__all__ = ["AnthropicGenerateContentConfig", "AnthropicLlm", "Claude"]
5252

5353
logger = logging.getLogger("google_adk." + __name__)
5454

55+
_THINKING_LEVEL_TO_EFFORT: dict[types.ThinkingLevel, str] = {
56+
types.ThinkingLevel.MINIMAL: "low",
57+
types.ThinkingLevel.LOW: "medium",
58+
types.ThinkingLevel.MEDIUM: "high",
59+
types.ThinkingLevel.HIGH: "xhigh",
60+
}
61+
62+
63+
# google-genai ships no py.typed marker / complete stubs, so mypy resolves
64+
# GenerateContentConfig as Any and flags the subclass. Composition would lose
65+
# isinstance() checks and Pydantic field inheritance; a local stub file would
66+
# need maintenance on every google-genai release. Suppressing narrowly is the
67+
# least-bad option.
68+
class AnthropicGenerateContentConfig(types.GenerateContentConfig): # type: ignore[misc]
69+
"""GenerateContentConfig extended with Anthropic-specific parameters."""
70+
71+
effort: Optional[Literal["low", "medium", "high", "xhigh", "max"]] = None
72+
73+
74+
def _build_effort_param(
75+
config: types.GenerateContentConfig,
76+
) -> Union[anthropic_types.OutputConfigParam, NotGiven]:
77+
"""Maps ADK config to Anthropic output_config.effort, or NOT_GIVEN."""
78+
if isinstance(config, AnthropicGenerateContentConfig) and config.effort:
79+
return anthropic_types.OutputConfigParam(effort=config.effort)
80+
if (
81+
config.thinking_config
82+
and config.thinking_config.thinking_level
83+
and config.thinking_config.thinking_level in _THINKING_LEVEL_TO_EFFORT
84+
):
85+
effort = _THINKING_LEVEL_TO_EFFORT[config.thinking_config.thinking_level]
86+
return anthropic_types.OutputConfigParam(effort=effort)
87+
return NOT_GIVEN
88+
5589

5690
@dataclasses.dataclass
5791
class _ToolUseAccumulator:
@@ -96,11 +130,9 @@ def _build_anthropic_thinking_param(
96130
thinking_budget = config.thinking_config.thinking_budget
97131

98132
if thinking_budget is None:
99-
raise ValueError(
100-
"thinking_budget must be set explicitly when ThinkingConfig is"
101-
" provided for Anthropic models. Use 0 to disable thinking, or a"
102-
" positive integer (>= 1024) for the token budget."
103-
)
133+
# thinking_level (effort) is being used instead of budget; thinking param
134+
# is not needed — output_config handles it.
135+
return NOT_GIVEN
104136

105137
if thinking_budget == 0:
106138
return anthropic_types.ThinkingConfigDisabledParam(type="disabled")
@@ -494,6 +526,8 @@ async def generate_content_async(
494526
thinking = _build_anthropic_thinking_param(llm_request.config)
495527

496528
config = llm_request.config
529+
effort = _build_effort_param(config)
530+
use_sampling = effort is NOT_GIVEN
497531
if not stream:
498532
message = await self._anthropic_client.messages.create(
499533
model=model_to_use,
@@ -503,9 +537,10 @@ async def generate_content_async(
503537
tool_choice=tool_choice,
504538
max_tokens=self.max_tokens,
505539
thinking=thinking,
506-
temperature=config.temperature if config.temperature is not None else NOT_GIVEN,
507-
top_p=config.top_p if config.top_p is not None else NOT_GIVEN,
508-
top_k=config.top_k if config.top_k is not None else NOT_GIVEN,
540+
output_config=effort,
541+
temperature=config.temperature if use_sampling and config.temperature is not None else NOT_GIVEN,
542+
top_p=config.top_p if use_sampling and config.top_p is not None else NOT_GIVEN,
543+
top_k=config.top_k if use_sampling and config.top_k is not None else NOT_GIVEN,
509544
stop_sequences=config.stop_sequences if config.stop_sequences is not None else NOT_GIVEN,
510545
)
511546
yield message_to_generate_content_response(message)
@@ -534,16 +569,19 @@ async def _generate_content_streaming(
534569
"""
535570
model_to_use = self._resolve_model_name(llm_request.model)
536571
config = llm_request.config
572+
effort = _build_effort_param(config)
573+
use_sampling = effort is NOT_GIVEN
537574
raw_stream = await self._anthropic_client.messages.create(
538575
model=model_to_use,
539576
system=config.system_instruction,
540577
messages=messages,
541578
tools=tools,
542579
tool_choice=tool_choice,
543580
max_tokens=self.max_tokens,
544-
temperature=config.temperature if config.temperature is not None else NOT_GIVEN,
545-
top_p=config.top_p if config.top_p is not None else NOT_GIVEN,
546-
top_k=config.top_k if config.top_k is not None else NOT_GIVEN,
581+
output_config=effort,
582+
temperature=config.temperature if use_sampling and config.temperature is not None else NOT_GIVEN,
583+
top_p=config.top_p if use_sampling and config.top_p is not None else NOT_GIVEN,
584+
top_k=config.top_k if use_sampling and config.top_k is not None else NOT_GIVEN,
547585
stop_sequences=config.stop_sequences if config.stop_sequences is not None else NOT_GIVEN,
548586
stream=True,
549587
thinking=thinking,

0 commit comments

Comments
 (0)