diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index a070a9ff215..2aa444497c3 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -53,6 +53,55 @@ def truncate_tool_name(name: str) -> str: _MCP_NAMESPACE_PATTERN = re.compile(r"^mcp__([^_]+(?:_[^_]+)*)__(.+)$") +# Pattern matching the wallet/MCP-style validation error returned by +# claude-agent-sdk when a tool call is missing a required field. Example: +# "Input validation error: 'to' is a required property" +_VALIDATION_REQUIRED_FIELD_PATTERN = re.compile( + r"(?:Input |MCP )?validation error[s]?[:\s]+'([^']+)' is a required property", + re.IGNORECASE, +) +# Companion pattern for the wrapped MCP variant the SDK emits, e.g. +# "Error: No such tool available: transfer" +_NO_SUCH_TOOL_PATTERN = re.compile( + r"No such tool available[:\s]+(\S+)", + re.IGNORECASE, +) + +# Hints appended to tool_result error messages on routes that go through +# the Anthropic→OpenAI passthrough. Empirically, Gemini-flash-lite +# tends to loop or give up on the raw error text; adding a concrete +# corrective instruction breaks the loop. The hint is intentionally +# explicit about WHERE to find the missing value (the user's prompt), +# which is the model's blind spot per the audit in #12. +_VALIDATION_HINT_TEMPLATE = ( + "\n\nCORRECTION: your previous tool_use call did not include the " + "required '{field}' argument. Look in the user's original message " + "for the value (e.g. recipient address, token symbol, amount, chain " + "name). Re-emit the tool_use with the same name but include the " + "'{field}' field populated. Do not ask the user for information " + "that is already in their message." +) + + +def augment_tool_result_error_for_correction(content: str) -> str: + """If a tool_result message contains a 'required property' validation + error or a 'No such tool available' error, append a corrective hint + that nudges weaker models toward fixing the call instead of looping + or asking the user for already-provided info. + + Pure string transformation; no behaviour for non-error tool_results. + See cryptitalk-wallet-litellm#12 for the failure mode this addresses. + """ + if not content or not isinstance(content, str): + return content + m = _VALIDATION_REQUIRED_FIELD_PATTERN.search(content) + if m is not None: + field = m.group(1) + if _VALIDATION_HINT_TEMPLATE.format(field=field) not in content: + return content + _VALIDATION_HINT_TEMPLATE.format(field=field) + return content + + def strip_mcp_namespace(name: str) -> str: """Strip the ``mcp____`` prefix from a tool name. @@ -469,7 +518,9 @@ class LiteLLMAnthropicMessagesAdapter: tool_result = ChatCompletionToolMessage( role="tool", tool_call_id=content.get("tool_use_id", ""), - content=str(content.get("content", "")), + content=augment_tool_result_error_for_correction( + str(content.get("content", "")) + ), ) self._add_cache_control_if_applicable( content, tool_result, model @@ -488,7 +539,9 @@ class LiteLLMAnthropicMessagesAdapter: tool_result = ChatCompletionToolMessage( role="tool", tool_call_id=content.get("tool_use_id", ""), - content=c, + content=augment_tool_result_error_for_correction( + c + ), ) self._add_cache_control_if_applicable( content, tool_result, model @@ -501,7 +554,9 @@ class LiteLLMAnthropicMessagesAdapter: tool_call_id=content.get( "tool_use_id", "" ), - content=c.get("text", ""), + content=augment_tool_result_error_for_correction( + c.get("text", "") + ), ) self._add_cache_control_if_applicable( content, tool_result, model