From 30c5d43b5bc213dedf50a0c785b274d12bd7789d Mon Sep 17 00:00:00 2001 From: Crypti Talk Date: Thu, 21 May 2026 23:13:17 +0000 Subject: [PATCH] fix(vertex_ai): rewrap claude-agent-sdk bare-field tool schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Anthropic /v1/messages tool input_schema spec is a JSON Schema object with type/properties/required. Some SDKs (notably claude-agent-sdk's @tool decorator) emit a non-conformant shape that puts field definitions at the top level with per-field required:true flags: {"to": {"type":"string","required":true}, "amount": {...}} Anthropic's own API tolerates this. Vertex/Gemini's strict Schema validator does not — filter_schema_fields strips the schema down to {"type":"object"} (empty), and the model then hallucinates field names from the tool description (e.g. dst_address, coin). Add _rewrap_bare_field_schema() that detects the bare-field shape and rewraps to standard JSON Schema before _build_vertex_schema processes it. Already-conformant schemas pass through unchanged. Test plan: - Locally exercised the helper against the bare-field shape, an already-conformant schema, empty {}, non-dict values, and dicts without type field — all behave correctly. - End-to-end: replayed a wallet adversarial-suite prompt through the LiteLLM gateway (Anthropic /v1/messages format with 61 tools in the claude-agent-sdk shape). Before patch: Gemini emitted tool_use(transfer, {dst_address: ...}). After patch: Gemini emitted tool_use(transfer, {to: ...}) matching MiniMax-via-gateway byte-for-byte on the same payload. --- litellm/llms/vertex_ai/common_utils.py | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index e6e39651109..5a142922db1 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -535,6 +535,42 @@ def _fix_enum_types(schema, depth=0): _fix_enum_types(item, depth=depth + 1) +def _rewrap_bare_field_schema(parameters: dict) -> dict: + """Rewrap claude-agent-sdk style "bare field" tool schemas in place. + + The Anthropic Messages API tool ``input_schema`` is documented as a JSON + Schema object with ``type``, ``properties``, ``required``. Some SDKs + (notably ``claude-agent-sdk``'s ``@tool`` decorator) emit a non-conformant + convenience shape that puts field definitions at the top level with + per-field ``required: true`` flags:: + + {"to": {"type": "string", "required": true}, "amount": {...}} + + Anthropic's own API tolerates this. Gemini's strict Schema validator + does not — without rewrapping, the downstream ``filter_schema_fields`` + call below strips the whole schema down to ``{"type": "object"}``, + causing the model to hallucinate field names when calling the tool. + """ + if not isinstance(parameters, dict) or not parameters: + return parameters + if parameters.get("type") == "object" and "properties" in parameters: + return parameters + # Detect the bare-field shape: every value is a dict with a "type" key. + if not all(isinstance(v, dict) and "type" in v for v in parameters.values()): + return parameters + properties: dict = {} + required: list = [] + for field, defn in parameters.items(): + defn_copy = {k: v for k, v in defn.items() if k != "required"} + if defn.get("required") is True: + required.append(field) + properties[field] = defn_copy + out: dict = {"type": "object", "properties": properties} + if required: + out["required"] = required + return out + + def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False): """ This is a modified version of https://github.com/google-gemini/generative-ai-python/blob/8f77cc6ac99937cd3a81299ecf79608b91b06bbb/google/generativeai/types/content_types.py#L419 @@ -548,6 +584,9 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False): Returns: parameters: dict - the input parameters, modified in place """ + # Pre-rewrap claude-agent-sdk bare-field schemas before strict filtering. + parameters = _rewrap_bare_field_schema(parameters) + # Get valid fields from Schema TypedDict valid_schema_fields = set(get_type_hints(Schema).keys())