Skip to content

Commit c41368d

Browse files
committed
feat: add thinking config for anthropic llm
1 parent 684a6e7 commit c41368d

2 files changed

Lines changed: 393 additions & 332 deletions

File tree

src/google/adk/models/anthropic_llm.py

Lines changed: 78 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -62,60 +62,11 @@ class _ToolUseAccumulator:
6262
args_json: str
6363

6464

65-
@dataclasses.dataclass
66-
class _ThinkingAccumulator:
65+
class _ThinkingAccumulator(BaseModel):
6766
"""Accumulates streamed thinking content block data."""
6867

69-
thinking: str
70-
signature: str
71-
72-
73-
def _build_anthropic_thinking_param(
74-
config: Optional[types.GenerateContentConfig],
75-
) -> Union[
76-
anthropic_types.ThinkingConfigEnabledParam,
77-
anthropic_types.ThinkingConfigDisabledParam,
78-
NotGiven,
79-
]:
80-
"""Maps genai ThinkingConfig to Anthropic's thinking parameter.
81-
82-
Per ``google.genai.types.ThinkingConfig``, ``thinking_budget`` semantics are:
83-
* ``None``: not specified; the genai default is model-dependent. Anthropic
84-
requires an explicit ``budget_tokens`` whenever thinking is enabled, so
85-
we surface this as a ``ValueError`` to keep the developer's intent
86-
explicit (mirroring the Anthropic API).
87-
* ``0``: thinking is DISABLED.
88-
* ``-1``: AUTOMATIC; not supported by Anthropic models.
89-
* positive int: budget in tokens (Anthropic requires ``>= 1024`` and
90-
``< max_tokens``; validation is delegated to the Anthropic API so the
91-
caller gets the canonical error message).
92-
"""
93-
if not config or not config.thinking_config:
94-
return NOT_GIVEN
95-
96-
thinking_budget = config.thinking_config.thinking_budget
97-
98-
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-
)
104-
105-
if thinking_budget == 0:
106-
return anthropic_types.ThinkingConfigDisabledParam(type="disabled")
107-
108-
if thinking_budget < 0:
109-
raise ValueError(
110-
f"thinking_budget={thinking_budget} is not supported for Anthropic"
111-
" models (AUTOMATIC mode is unavailable). Use a positive integer"
112-
" (>= 1024) for the token budget, or 0 to disable thinking."
113-
)
114-
115-
return anthropic_types.ThinkingConfigEnabledParam(
116-
type="enabled",
117-
budget_tokens=thinking_budget,
118-
)
68+
thinking: str = ""
69+
signature: str = ""
11970

12071

12172
class ClaudeRequest(BaseModel):
@@ -165,23 +116,24 @@ def part_to_message_block(
165116
anthropic_types.DocumentBlockParam,
166117
anthropic_types.ToolUseBlockParam,
167118
anthropic_types.ToolResultBlockParam,
119+
anthropic_types.ThinkingBlockParam,
120+
anthropic_types.RedactedThinkingBlockParam,
168121
]:
169-
if part.thought and part.text:
170-
signature = ""
171-
if part.thought_signature:
172-
signature = part.thought_signature.decode("utf-8")
173-
return anthropic_types.ThinkingBlockParam(
174-
type="thinking",
175-
thinking=part.text,
176-
signature=signature,
177-
)
178-
if part.thought and part.thought_signature:
179-
# Redacted thinking: no plaintext, only the encrypted blob produced by
180-
# content_block_to_part for round-tripping back to Claude.
181-
return anthropic_types.RedactedThinkingBlockParam(
182-
type="redacted_thinking",
183-
data=part.thought_signature.decode("utf-8"),
122+
if part.thought:
123+
signature_str = (
124+
part.thought_signature.decode("utf-8") if part.thought_signature else ""
184125
)
126+
if part.text:
127+
return anthropic_types.ThinkingBlockParam(
128+
type="thinking",
129+
thinking=part.text,
130+
signature=signature_str,
131+
)
132+
else:
133+
return anthropic_types.RedactedThinkingBlockParam(
134+
type="redacted_thinking",
135+
data=signature_str,
136+
)
185137
if part.text:
186138
return anthropic_types.TextBlockParam(text=part.text, type="text")
187139
elif part.function_call:
@@ -315,9 +267,19 @@ def content_block_to_part(
315267
)
316268
part.function_call.id = content_block.id
317269
return part
318-
raise NotImplementedError(
319-
f"Unsupported content block type: {type(content_block)}"
320-
)
270+
if isinstance(content_block, anthropic_types.ThinkingBlock):
271+
return types.Part(
272+
text=content_block.thinking,
273+
thought=True,
274+
thought_signature=content_block.signature.encode("utf-8"),
275+
)
276+
if isinstance(content_block, anthropic_types.RedactedThinkingBlock):
277+
return types.Part(
278+
text="",
279+
thought=True,
280+
thought_signature=content_block.data.encode("utf-8"),
281+
)
282+
raise NotImplementedError("Not supported yet.")
321283

322284

323285
def message_to_generate_content_response(
@@ -439,6 +401,26 @@ def function_declaration_to_tool_param(
439401
)
440402

441403

404+
def _build_thinking_param(
405+
thinking_config: Optional[types.ThinkingConfig],
406+
max_tokens: int,
407+
) -> Union[anthropic_types.ThinkingConfigEnabledParam, NotGiven]:
408+
"""Converts ADK ThinkingConfig to Anthropic ThinkingConfigEnabledParam.
409+
410+
Returns NOT_GIVEN if thinking is not configured or budget is 0.
411+
Clamps budget_tokens to max_tokens - 1 to satisfy the API constraint.
412+
"""
413+
if thinking_config is None:
414+
return NOT_GIVEN
415+
budget = thinking_config.thinking_budget
416+
if not budget:
417+
return NOT_GIVEN
418+
return anthropic_types.ThinkingConfigEnabledParam(
419+
type="enabled",
420+
budget_tokens=min(budget, max_tokens - 1),
421+
)
422+
423+
442424
class AnthropicLlm(BaseLlm):
443425
"""Integration with Claude models via the Anthropic API.
444426
@@ -491,7 +473,10 @@ async def generate_content_async(
491473
if llm_request.tools_dict
492474
else NOT_GIVEN
493475
)
494-
thinking = _build_anthropic_thinking_param(llm_request.config)
476+
thinking = _build_thinking_param(
477+
llm_request.config.thinking_config if llm_request.config else None,
478+
self.max_tokens,
479+
)
495480

496481
if not stream:
497482
message = await self._anthropic_client.messages.create(
@@ -517,9 +502,7 @@ async def _generate_content_streaming(
517502
tools: Union[Iterable[anthropic_types.ToolUnionParam], NotGiven],
518503
tool_choice: Union[anthropic_types.ToolChoiceParam, NotGiven],
519504
thinking: Union[
520-
anthropic_types.ThinkingConfigEnabledParam,
521-
anthropic_types.ThinkingConfigDisabledParam,
522-
NotGiven,
505+
anthropic_types.ThinkingConfigEnabledParam, NotGiven
523506
] = NOT_GIVEN,
524507
) -> AsyncGenerator[LlmResponse, None]:
525508
"""Handles streaming responses from Anthropic models.
@@ -571,6 +554,10 @@ async def _generate_content_streaming(
571554
name=block.name,
572555
args_json="",
573556
)
557+
elif isinstance(block, anthropic_types.ThinkingBlock):
558+
thinking_blocks[event.index] = _ThinkingAccumulator()
559+
elif isinstance(block, anthropic_types.RedactedThinkingBlock):
560+
redacted_thinking_blocks[event.index] = block.data
574561

575562
elif event.type == "content_block_delta":
576563
delta = event.delta
@@ -600,6 +587,12 @@ async def _generate_content_streaming(
600587
elif isinstance(delta, anthropic_types.InputJSONDelta):
601588
if event.index in tool_use_blocks:
602589
tool_use_blocks[event.index].args_json += delta.partial_json
590+
elif isinstance(delta, anthropic_types.ThinkingDelta):
591+
if event.index in thinking_blocks:
592+
thinking_blocks[event.index].thinking += delta.thinking
593+
elif isinstance(delta, anthropic_types.SignatureDelta):
594+
if event.index in thinking_blocks:
595+
thinking_blocks[event.index].signature = delta.signature
603596

604597
elif event.type == "message_delta":
605598
output_tokens = event.usage.output_tokens
@@ -608,22 +601,26 @@ async def _generate_content_streaming(
608601
all_parts: list[types.Part] = []
609602
all_indices = sorted(
610603
set(
611-
list(thinking_blocks.keys())
612-
+ list(redacted_thinking_blocks.keys())
613-
+ list(text_blocks.keys())
604+
list(text_blocks.keys())
614605
+ list(tool_use_blocks.keys())
606+
+ list(thinking_blocks.keys())
607+
+ list(redacted_thinking_blocks.keys())
615608
)
616609
)
617610
for idx in all_indices:
618611
if idx in thinking_blocks:
619612
acc = thinking_blocks[idx]
620-
part = types.Part(text=acc.thinking, thought=True)
621-
if acc.signature:
622-
part.thought_signature = acc.signature.encode("utf-8")
623-
all_parts.append(part)
613+
all_parts.append(
614+
types.Part(
615+
text=acc.thinking,
616+
thought=True,
617+
thought_signature=acc.signature.encode("utf-8"),
618+
)
619+
)
624620
if idx in redacted_thinking_blocks:
625621
all_parts.append(
626622
types.Part(
623+
text="",
627624
thought=True,
628625
thought_signature=redacted_thinking_blocks[idx].encode("utf-8"),
629626
)

0 commit comments

Comments
 (0)