litellm - 💡(How to fix) Fix Gemini hallucinates tool argument names when input_schema is in claude-agent-sdk's 'bare field' shape

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When the wallet's tools are defined via claude-agent-sdk's @tool decorator, the input_schema is emitted in a non-standard "bare field" shape:

{
  "to": {"type": "string", "description": "Recipient", "required": true},
  "amount": {"type": "string", "description": "Amount", "required": true},
  "token": {"type": "string", "description": "Token symbol"}
}

Anthropic's /v1/messages API tolerates this. MiniMax (Anthropic-compatible upstream) tolerates this. Gemini does not — LiteLLM's _build_vertex_schema correctly looks for properties (per the JSON Schema spec), finds none, and filter_schema_fields strips the schema down to {"type": "object"}.

The Gemini model then receives an empty schema for a tool named transfer and hallucinates field names from the tool description text (e.g., dst_address instead of to, coin instead of token).

Root Cause

litellm/llms/vertex_ai/common_utils.py:_build_vertex_schema expects standard JSON Schema input ({type: "object", properties: {...}, required: [...]}). The bare-field shape doesn't match, so filter_schema_fields strips everything that isn't in Gemini's Schema TypedDict — including all property definitions, which live at the top level instead of under properties.

Code Example

{
  "to": {"type": "string", "description": "Recipient", "required": true},
  "amount": {"type": "string", "description": "Amount", "required": true},
  "token": {"type": "string", "description": "Token symbol"}
}

---

@tool("transfer", "Send tokens to an address.", {
    "to":     {"type": "string", "description": "Recipient", "required": True},
    "amount": {"type": "string", "description": "Amount",    "required": True},
    "token":  {"type": "string", "description": "Token symbol"},
})

---

{
  "model": "gemini-2.5-flash-lite",
  "tools": [{"name": "transfer", "description": "...", "input_schema": {...as above...}}],
  "messages": [{"role": "user", "content": "send 0.001 ETH to 0xdead... on arbitrum"}]
}

---

{
  "content": [{
    "type": "tool_use",
    "name": "transfer",
    "input": {"amount": "0.001", "chain": "arbitrum", "dst_address": "0xdead..."}
  }]
}

---

def _rewrap_bare_field_schema(parameters: dict) -> dict:
    if not isinstance(parameters, dict) or not parameters:
        return parameters
    if parameters.get("type") == "object" and "properties" in parameters:
        return parameters
    if not all(isinstance(v, dict) and "type" in v for v in parameters.values()):
        return parameters
    properties, required = {}, []
    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 = {"type": "object", "properties": properties}
    if required:
        out["required"] = required
    return out
RAW_BUFFERClick to expand / collapse

Summary

When the wallet's tools are defined via claude-agent-sdk's @tool decorator, the input_schema is emitted in a non-standard "bare field" shape:

{
  "to": {"type": "string", "description": "Recipient", "required": true},
  "amount": {"type": "string", "description": "Amount", "required": true},
  "token": {"type": "string", "description": "Token symbol"}
}

Anthropic's /v1/messages API tolerates this. MiniMax (Anthropic-compatible upstream) tolerates this. Gemini does not — LiteLLM's _build_vertex_schema correctly looks for properties (per the JSON Schema spec), finds none, and filter_schema_fields strips the schema down to {"type": "object"}.

The Gemini model then receives an empty schema for a tool named transfer and hallucinates field names from the tool description text (e.g., dst_address instead of to, coin instead of token).

Reproduction

Tool definition (via claude-agent-sdk @tool):

@tool("transfer", "Send tokens to an address.", {
    "to":     {"type": "string", "description": "Recipient", "required": True},
    "amount": {"type": "string", "description": "Amount",    "required": True},
    "token":  {"type": "string", "description": "Token symbol"},
})

Send via LiteLLM proxy /v1/messages to gemini-2.5-flash-lite:

{
  "model": "gemini-2.5-flash-lite",
  "tools": [{"name": "transfer", "description": "...", "input_schema": {...as above...}}],
  "messages": [{"role": "user", "content": "send 0.001 ETH to 0xdead... on arbitrum"}]
}

Result:

{
  "content": [{
    "type": "tool_use",
    "name": "transfer",
    "input": {"amount": "0.001", "chain": "arbitrum", "dst_address": "0xdead..."}
  }]
}

The to field is renamed to dst_address, hallucinated by the model from the description text.

Sending the same payload to MiniMax-M2.7-highspeed (Anthropic-compatible upstream) through the same proxy yields the correct to field name.

Root cause

litellm/llms/vertex_ai/common_utils.py:_build_vertex_schema expects standard JSON Schema input ({type: "object", properties: {...}, required: [...]}). The bare-field shape doesn't match, so filter_schema_fields strips everything that isn't in Gemini's Schema TypedDict — including all property definitions, which live at the top level instead of under properties.

Proposed fix

Pre-rewrap the bare-field shape into proper JSON Schema before _build_vertex_schema's strict filtering:

def _rewrap_bare_field_schema(parameters: dict) -> dict:
    if not isinstance(parameters, dict) or not parameters:
        return parameters
    if parameters.get("type") == "object" and "properties" in parameters:
        return parameters
    if not all(isinstance(v, dict) and "type" in v for v in parameters.values()):
        return parameters
    properties, required = {}, []
    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 = {"type": "object", "properties": properties}
    if required:
        out["required"] = required
    return out

Then add parameters = _rewrap_bare_field_schema(parameters) as the first line in _build_vertex_schema.

The conditions on the rewrap are tight (every value must be a dict with a type key; existing type: object + properties schemas are no-ops), so it doesn't affect any conformant input.

Want me to send a PR?

I have the fix on a fork branch with tests covering: bare-field, already-conformant, empty {}, non-dict values, and dicts missing type. Happy to send a PR if maintainers are interested.

LiteLLM version tested: v1.82.6

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING