@@ -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
12172class 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
323285def 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+
442424class 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