diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 9babc37c869..18ae2530ed5 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -662,7 +662,9 @@ class AnthropicMessagesHandler(BaseTranslation): ) if not scan_only_tool_results: retained_names: Final = frozenset( - name for t in (data.get("tools") or []) for name in anthropic_tool_names(t) + name + for t in (data.get("tools") or ()) + for name in anthropic_tool_names(t) # comprehension-ok: flattens tool names across tools ) self._strip_disallowed_tool_additions(data, retained_names) @@ -940,55 +942,71 @@ class AnthropicMessagesHandler(BaseTranslation): """Extract every tool name in an Anthropic messages request: tools[].name, plus tools[].function.name for OpenAI-format tools the bridge forwards verbatim, as well as mid-conversation tool additions in messages.""" - names = [name for tool in list_field(data, "tools") for name in anthropic_tool_names(tool)] - messages = data.get("messages") - if isinstance(messages, Sequence): - for m in messages: - if isinstance(m, Mapping): - content = m.get("content") - if isinstance(content, Sequence) and not isinstance(content, (str, bytes)): - for block in content: - if isinstance(block, Mapping) and block.get("type") == "tool_addition": - tool = extract_tool_from_addition_block(block) - if tool is not None: - for name in anthropic_tool_names(tool): - if name not in names: - names.append(name) - return names + top_names: Final = [ # comprehension-ok: flattens tool names across tools + name for tool in list_field(data, "tools") for name in anthropic_tool_names(tool) + ] + messages: Final = data.get("messages") + if not isinstance(messages, Sequence): + return top_names + extra_names: Final[list[str]] = [] # mutable-ok: accumulator + for m in messages: + if isinstance(m, Mapping): + content: Final = m.get("content") + if isinstance(content, Sequence) and not isinstance(content, (str, bytes)): + for block in content: + if isinstance(block, Mapping) and block.get("type") == "tool_addition": + tool: Final = extract_tool_from_addition_block(block) + if tool is not None: + for name in anthropic_tool_names(tool): + if name not in top_names and name not in extra_names: + extra_names.append(name) + return [*top_names, *extra_names] @classmethod - def _strip_disallowed_tool_additions(cls, data: dict, retained_names: frozenset[str]) -> None: - messages = data.get("messages") + def _strip_disallowed_tool_additions(cls, data: Mapping[str, object], retained_names: frozenset[str]) -> None: + messages: Final = data.get("messages") if not isinstance(messages, list): return - cleaned_messages: list[object] = [] + cleaned_messages: Final[list[object]] = [] # mutable-ok: accumulator for m in messages: if not isinstance(m, dict): cleaned_messages.append(m) continue - content = m.get("content") + content: Final = m.get("content") if not isinstance(content, list): cleaned_messages.append(m) continue - had_disallowed_addition = False - remaining_blocks: list[object] = [] - for block in content: - if isinstance(block, dict) and block.get("type") == "tool_addition": - tool = extract_tool_from_addition_block(block) - tool_names = anthropic_tool_names(tool) if tool else () - if tool_names and all(name in retained_names for name in tool_names): - remaining_blocks.append(block) - else: - had_disallowed_addition = True - else: - remaining_blocks.append(block) + had_disallowed_addition: Final = any( + isinstance(block, dict) + and block.get("type") == "tool_addition" + and ( + not (tool := extract_tool_from_addition_block(block)) + or not (tool_names := anthropic_tool_names(tool)) + or not all(name in retained_names for name in tool_names) + ) + for block in content + ) + if not had_disallowed_addition: + cleaned_messages.append(m) + continue + remaining_blocks: Final[list[object]] = [ # mutable-ok: accumulator + block + for block in content + if not ( + isinstance(block, dict) + and block.get("type") == "tool_addition" + and ( + not (tool := extract_tool_from_addition_block(block)) + or not (tool_names := anthropic_tool_names(tool)) + or not all(name in retained_names for name in tool_names) + ) + ) + ] if remaining_blocks: - new_m = dict(m) + new_m: Final[dict[str, object]] = dict(m) # mutable-ok: message clone new_m["content"] = remaining_blocks cleaned_messages.append(new_m) - elif not had_disallowed_addition: - cleaned_messages.append(m) - data["messages"] = cleaned_messages + data["messages"] = cleaned_messages # rebind-ok: update request in place @classmethod def _extract_input_text_and_images( diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 8db9e9155b6..867bc734877 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -1236,73 +1236,78 @@ class LiteLLMAnthropicMessagesAdapter: self, messages: Sequence[Mapping[str, object] | AllAnthropicPassThroughMessageValues], initial_tools: Sequence[AllAnthropicToolsValues] | None = None, - ) -> tuple[list[AllAnthropicToolsValues] | None, list[AllAnthropicPassThroughMessageValues]]: + ) -> tuple[tuple[AllAnthropicToolsValues, ...] | None, tuple[AllAnthropicPassThroughMessageValues, ...]]: """ Extract and apply mid-conversation tool changes (tool_addition, tool_removal) such as those sent by Claude Code under mid-conversation-tool-changes beta. Returns: Tuple of (active_tools, cleaned_messages) - - active_tools: Updated tools list with additions applied and removals removed, + - active_tools: Updated tools tuple with additions applied and removals removed, or None if no tools exist and none were added. - - cleaned_messages: Messages list with tool_addition and tool_removal blocks stripped, + - cleaned_messages: Messages tuple with tool_addition and tool_removal blocks stripped, and carrier messages left without content dropped. """ - active_tools: list[AllAnthropicToolsValues] = list(initial_tools) if initial_tools else [] - had_tool_changes = False - cleaned_messages: list[AllAnthropicPassThroughMessageValues] = [] + active_tools: tuple[AllAnthropicToolsValues, ...] = tuple(initial_tools or ()) # rebind-ok: tool updates + had_tool_changes: bool = False # rebind-ok: state flag + cleaned_messages: Final[list[AllAnthropicPassThroughMessageValues]] = [] # mutable-ok: accumulator for m in messages: - if not isinstance(m, dict): - cleaned_messages.append(m) # type: ignore[arg-type] + if not isinstance(m, Mapping): + cleaned_messages.append(m) # pyright: ignore[reportArgumentType] # non-mapping passthrough continue - content = m.get("content") - if not isinstance(content, list): - cleaned_messages.append(cast(AllAnthropicPassThroughMessageValues, m)) + content: Final = m.get("content") + if not isinstance(content, Sequence) or isinstance(content, (str, bytes)): + msg_val: Final = cast(AllAnthropicPassThroughMessageValues, m) # cast-ok: msg + cleaned_messages.append(msg_val) continue - remaining_blocks: list[dict[str, Any]] = [] + remaining_blocks: Final[list[object]] = [] # mutable-ok: accumulator for block in content: - if not isinstance(block, dict): + if not isinstance(block, Mapping): remaining_blocks.append(block) continue - btype = block.get("type") + btype: Final = block.get("type") if btype == "tool_addition": had_tool_changes = True - raw_tool = extract_tool_from_addition_block(block) - if isinstance(raw_tool, dict) and raw_tool.get("name"): - tool_name = str(raw_tool["name"]) - if not any(isinstance(t, dict) and t.get("name") == tool_name for t in active_tools): - active_tools.append(cast(AllAnthropicToolsValues, raw_tool)) + raw_tool: Final = extract_tool_from_addition_block(block) + if isinstance(raw_tool, Mapping) and raw_tool.get("name"): + tool_name: Final = str(raw_tool["name"]) + if not any(isinstance(t, Mapping) and t.get("name") == tool_name for t in active_tools): + added_tool: Final = cast(AllAnthropicToolsValues, dict(raw_tool)) # cast-ok: tool + active_tools = (*active_tools, added_tool) elif btype == "tool_removal": had_tool_changes = True - rem_name = ( + rem_name: Final = ( block.get("tool_name") or block.get("name") - or (block.get("tool", {}).get("name") if isinstance(block.get("tool"), dict) else None) + or (block.get("tool", {}).get("name") if isinstance(block.get("tool"), Mapping) else None) or (block.get("tool") if isinstance(block.get("tool"), str) else None) ) if rem_name: - rem_name_str = str(rem_name) - active_tools = [ - t for t in active_tools if not (isinstance(t, dict) and t.get("name") == rem_name_str) - ] + rem_name_str: Final = str(rem_name) + active_tools = tuple( + t for t in active_tools if not (isinstance(t, Mapping) and t.get("name") == rem_name_str) + ) else: remaining_blocks.append(block) if remaining_blocks: - new_m = dict(m) + new_m: Final[dict[str, object]] = dict(m) # mutable-ok: message clone new_m["content"] = remaining_blocks - cleaned_messages.append(cast(AllAnthropicPassThroughMessageValues, new_m)) + new_msg: Final = cast(AllAnthropicPassThroughMessageValues, new_m) # cast-ok: msg + cleaned_messages.append(new_msg) elif not had_tool_changes: - cleaned_messages.append(cast(AllAnthropicPassThroughMessageValues, m)) + clean_msg: Final = cast(AllAnthropicPassThroughMessageValues, m) # cast-ok: msg + cleaned_messages.append(clean_msg) if not had_tool_changes and initial_tools is None: - return None, cleaned_messages + return None, tuple(cleaned_messages) - return active_tools if active_tools else ( - [] if initial_tools is not None or had_tool_changes else None - ), cleaned_messages + return ( + active_tools if active_tools else (() if initial_tools is not None or had_tool_changes else None), + tuple(cleaned_messages), + ) def translate_anthropic_to_openai( self, @@ -1320,8 +1325,6 @@ class LiteLLMAnthropicMessagesAdapter: - tool_name_mapping maps truncated tool names back to original names for tools that exceeded OpenAI's 64-char limit """ - # Debug: Processing Anthropic message request - new_messages: list[AllMessageValues] = [] tool_name_mapping: dict[str, str] = {} ## CONVERT ANTHROPIC MESSAGES TO OPENAI @@ -1336,17 +1339,17 @@ class LiteLLMAnthropicMessagesAdapter: messages=messages_list, initial_tools=anthropic_message_request.get("tools"), ) - request_for_translation: Final[AnthropicMessagesRequest] = cast( + request_for_translation: Final[AnthropicMessagesRequest] = cast( # cast-ok: anthropic request type AnthropicMessagesRequest, dict(anthropic_message_request) ) if active_tools is not None: if active_tools: - request_for_translation["tools"] = active_tools + request_for_translation["tools"] = list(active_tools) else: request_for_translation.pop("tools", None) - new_messages = self.translate_anthropic_messages_to_openai( - messages=cleaned_messages, + new_messages: Final = self.translate_anthropic_messages_to_openai( + messages=list(cleaned_messages), model=request_for_translation.get("model"), custom_llm_provider=custom_llm_provider, preserve_midturn_system=preserve_midturn_system, diff --git a/litellm/llms/base_llm/guardrail_translation/utils.py b/litellm/llms/base_llm/guardrail_translation/utils.py index 9228da3cfcd..a4e587c36f3 100644 --- a/litellm/llms/base_llm/guardrail_translation/utils.py +++ b/litellm/llms/base_llm/guardrail_translation/utils.py @@ -319,22 +319,22 @@ def anthropic_tool_name(tool: object) -> str | None: return names[0] if names else None -def extract_tool_from_addition_block(block: object) -> dict[str, object] | None: +def extract_tool_from_addition_block(block: object) -> Mapping[str, object] | None: """Extract raw tool dict from an Anthropic tool_addition block.""" if not isinstance(block, Mapping): return None - tool_data = block.get("tool") - if isinstance(tool_data, dict): + tool_data: Final = block.get("tool") + if isinstance(tool_data, Mapping): if ( tool_data.get("type") == "tool_definition" and "definition" in tool_data - and isinstance(tool_data["definition"], dict) + and isinstance(tool_data["definition"], Mapping) ): return tool_data["definition"] return tool_data - if "definition" in block and isinstance(block["definition"], dict): + if "definition" in block and isinstance(block["definition"], Mapping): return block["definition"] - if isinstance(block.get("tool_definition"), dict): + if isinstance(block.get("tool_definition"), Mapping): return block["tool_definition"] return None