@@ -330,23 +330,19 @@ async def _messages_create(
330330 model_request_parameters : ModelRequestParameters ,
331331 ) -> BetaMessage | AsyncStream [BetaRawMessageStreamEvent ]:
332332 # standalone function to make it easier to override
333- tools , strict_tools_requested = self ._get_tools (model_request_parameters , model_settings )
334- tools , mcp_servers , beta_features = self ._add_builtin_tools (tools , model_request_parameters )
333+ tools = self ._get_tools (model_request_parameters , model_settings )
334+ tools , mcp_servers , builtin_tool_betas = self ._add_builtin_tools (tools , model_request_parameters )
335335 native_format = self ._native_output_format (model_request_parameters )
336336
337337 tool_choice = self ._infer_tool_choice (tools , model_settings , model_request_parameters )
338338
339339 system_prompt , anthropic_messages = await self ._map_message (messages , model_request_parameters , model_settings )
340340
341- # Build betas list for SDK
342- betas : list [str ] = list (beta_features )
343- if strict_tools_requested or native_format :
344- betas .append ('structured-outputs-2025-11-13' )
341+ betas = self ._get_required_betas (model_request_parameters )
342+ betas .update (builtin_tool_betas )
345343
346344 try :
347- # We use SDK's betas parameter instead of manual header manipulation
348- extra_headers = model_settings .get ('extra_headers' , {})
349- extra_headers .setdefault ('User-Agent' , get_user_agent ())
345+ betas_list , extra_headers = self ._prepare_betas_and_headers (betas , model_settings )
350346
351347 return await self .client .beta .messages .create (
352348 max_tokens = model_settings .get ('max_tokens' , 4096 ),
@@ -357,7 +353,7 @@ async def _messages_create(
357353 tool_choice = tool_choice or OMIT ,
358354 mcp_servers = mcp_servers or OMIT ,
359355 output_format = native_format or OMIT ,
360- betas = betas or OMIT ,
356+ betas = betas_list or OMIT ,
361357 stream = stream ,
362358 thinking = model_settings .get ('anthropic_thinking' , OMIT ),
363359 stop_sequences = model_settings .get ('stop_sequences' , OMIT ),
@@ -383,15 +379,18 @@ async def _messages_count_tokens(
383379 raise UserError ('AsyncAnthropicBedrock client does not support `count_tokens` api.' )
384380
385381 # standalone function to make it easier to override
386- tools , _ = self ._get_tools (model_request_parameters , model_settings )
387- tools , mcp_servers , beta_features = self ._add_builtin_tools (tools , model_request_parameters )
382+ tools = self ._get_tools (model_request_parameters , model_settings )
383+ tools , mcp_servers , builtin_tool_betas = self ._add_builtin_tools (tools , model_request_parameters )
388384
389385 tool_choice = self ._infer_tool_choice (tools , model_settings , model_request_parameters )
390386
391387 system_prompt , anthropic_messages = await self ._map_message (messages , model_request_parameters , model_settings )
392388
389+ betas = self ._get_required_betas (model_request_parameters )
390+ betas .update (builtin_tool_betas )
391+
393392 try :
394- extra_headers = self ._map_extra_headers ( beta_features , model_settings )
393+ betas_list , extra_headers = self ._prepare_betas_and_headers ( betas , model_settings )
395394
396395 return await self .client .beta .messages .count_tokens (
397396 system = system_prompt or OMIT ,
@@ -400,6 +399,7 @@ async def _messages_count_tokens(
400399 tools = tools or OMIT ,
401400 tool_choice = tool_choice or OMIT ,
402401 mcp_servers = mcp_servers or OMIT ,
402+ betas = betas_list or OMIT ,
403403 thinking = model_settings .get ('anthropic_thinking' , OMIT ),
404404 timeout = model_settings .get ('timeout' , NOT_GIVEN ),
405405 extra_headers = extra_headers ,
@@ -485,13 +485,10 @@ async def _process_streamed_response(
485485
486486 def _get_tools (
487487 self , model_request_parameters : ModelRequestParameters , model_settings : AnthropicModelSettings
488- ) -> tuple [ list [BetaToolUnionParam ], bool ]:
488+ ) -> list [BetaToolUnionParam ]:
489489 tools : list [BetaToolUnionParam ] = []
490- strict_tools_requested = False
491490 for tool_def in model_request_parameters .tool_defs .values ():
492491 tools .append (self ._map_tool_definition (tool_def ))
493- if tool_def .strict and self .profile .supports_json_schema_output :
494- strict_tools_requested = True
495492
496493 # Add cache_control to the last tool if enabled
497494 if tools and (cache_tool_defs := model_settings .get ('anthropic_cache_tool_definitions' )):
@@ -500,12 +497,35 @@ def _get_tools(
500497 last_tool = tools [- 1 ]
501498 last_tool ['cache_control' ] = BetaCacheControlEphemeralParam (type = 'ephemeral' , ttl = ttl )
502499
503- return tools , strict_tools_requested
500+ return tools
501+
502+ def _get_required_betas (self , model_request_parameters : ModelRequestParameters ) -> set [str ]:
503+ """Determine which beta features are needed based on tools and output format.
504+
505+ Args:
506+ model_request_parameters: Model request parameters containing tools and output settings
507+
508+ Returns:
509+ Set of beta feature strings (naturally deduplicated)
510+ """
511+ betas : set [str ] = set ()
512+
513+ has_strict_tools = any (
514+ tool_def .strict and self .profile .supports_json_schema_output
515+ for tool_def in model_request_parameters .tool_defs .values ()
516+ )
517+
518+ has_native_output = model_request_parameters .output_mode == 'native'
519+
520+ if has_strict_tools or has_native_output :
521+ betas .add ('structured-outputs-2025-11-13' )
522+
523+ return betas
504524
505525 def _add_builtin_tools (
506526 self , tools : list [BetaToolUnionParam ], model_request_parameters : ModelRequestParameters
507- ) -> tuple [list [BetaToolUnionParam ], list [BetaRequestMCPServerURLDefinitionParam ], list [str ]]:
508- beta_features : list [str ] = []
527+ ) -> tuple [list [BetaToolUnionParam ], list [BetaRequestMCPServerURLDefinitionParam ], set [str ]]:
528+ beta_features : set [str ] = set ()
509529 mcp_servers : list [BetaRequestMCPServerURLDefinitionParam ] = []
510530 for tool in model_request_parameters .builtin_tools :
511531 if isinstance (tool , WebSearchTool ):
@@ -522,14 +542,14 @@ def _add_builtin_tools(
522542 )
523543 elif isinstance (tool , CodeExecutionTool ): # pragma: no branch
524544 tools .append (BetaCodeExecutionTool20250522Param (name = 'code_execution' , type = 'code_execution_20250522' ))
525- beta_features .append ('code-execution-2025-05-22' )
545+ beta_features .add ('code-execution-2025-05-22' )
526546 elif isinstance (tool , MemoryTool ): # pragma: no branch
527547 if 'memory' not in model_request_parameters .tool_defs :
528548 raise UserError ("Built-in `MemoryTool` requires a 'memory' tool to be defined." )
529549 # Replace the memory tool definition with the built-in memory tool
530550 tools = [tool for tool in tools if tool ['name' ] != 'memory' ]
531551 tools .append (BetaMemoryTool20250818Param (name = 'memory' , type = 'memory_20250818' ))
532- beta_features .append ('context-management-2025-06-27' )
552+ beta_features .add ('context-management-2025-06-27' )
533553 elif isinstance (tool , MCPServerTool ) and tool .url :
534554 mcp_server_url_definition_param = BetaRequestMCPServerURLDefinitionParam (
535555 type = 'url' ,
@@ -544,7 +564,7 @@ def _add_builtin_tools(
544564 if tool .authorization_token : # pragma: no cover
545565 mcp_server_url_definition_param ['authorization_token' ] = tool .authorization_token
546566 mcp_servers .append (mcp_server_url_definition_param )
547- beta_features .append ('mcp-client-2025-04-04' )
567+ beta_features .add ('mcp-client-2025-04-04' )
548568 else : # pragma: no cover
549569 raise UserError (
550570 f'`{ tool .__class__ .__name__ } ` is not supported by `AnthropicModel`. If it should be, please file an issue.'
@@ -572,15 +592,33 @@ def _infer_tool_choice(
572592
573593 return tool_choice
574594
575- def _map_extra_headers (self , beta_features : list [str ], model_settings : AnthropicModelSettings ) -> dict [str , str ]:
576- """Apply beta_features to extra_headers in model_settings."""
595+ def _prepare_betas_and_headers (
596+ self , betas : set [str ], model_settings : AnthropicModelSettings
597+ ) -> tuple [list [str ], dict [str , str ]]:
598+ """Prepare beta features list and extra headers for API request.
599+
600+ Handles merging custom anthropic-beta header from extra_headers into betas set
601+ and ensuring User-Agent is set.
602+
603+ Args:
604+ betas: Set of beta feature strings (naturally deduplicated)
605+ model_settings: Model settings containing extra_headers
606+
607+ Returns:
608+ Tuple of (betas list, extra_headers dict)
609+ """
577610 extra_headers = model_settings .get ('extra_headers' , {})
578611 extra_headers .setdefault ('User-Agent' , get_user_agent ())
579- if beta_features :
580- if 'anthropic-beta' in extra_headers :
581- beta_features .insert (0 , extra_headers ['anthropic-beta' ])
582- extra_headers ['anthropic-beta' ] = ',' .join (beta_features )
583- return extra_headers
612+
613+ if 'anthropic-beta' in extra_headers :
614+ beta_value = extra_headers ['anthropic-beta' ]
615+ for beta in beta_value .split (',' ):
616+ beta_stripped = beta .strip ()
617+ if beta_stripped :
618+ betas .add (beta_stripped )
619+ del extra_headers ['anthropic-beta' ]
620+
621+ return sorted (betas ), extra_headers
584622
585623 async def _map_message ( # noqa: C901
586624 self ,
0 commit comments