Skip to content

LiteLlm raises out of response parsing on some unusable tool call arguments instead of reporting MALFORMED_FUNCTION_CALL #7802

Closed

Description

@yasumorishima

🔴 Required Information

Describe the Bug:
When a model returns tool call arguments that cannot become a function call, LiteLlm reports the response as MALFORMED_FUNCTION_CALL and keeps the text. A few shapes of arguments raise out of response parsing instead. In the non-streaming path these are an unhashable key such as {[]: 1}, a non-string key such as {1: 2}, an integer over 4,300 digits, and very deep nesting. The streaming path raises on the unhashable key, the huge integer and the deep nesting, and turns {1: 2} into {"1": 2}. It also raises on a set or bytes value from the ast.literal_eval repair, and on an object nested a couple of thousand levels.

Steps to Reproduce:

  1. Install google-adk from main (3640e7d) with litellm 1.106.2.
  2. Run the snippet below.

Expected Behavior:
Each case is reported as MALFORMED_FUNCTION_CALL, like other unparseable arguments.

Observed Behavior:

unhashable key: raised TypeError
non-string key: raised ValidationError
huge integer: raised ValueError
deep nesting: raised RecursionError

Environment Details:

  • ADK Library Version (pip show google-adk): 2.13.0 (main at 3640e7d)
  • Desktop OS: Linux (aarch64)
  • Python Version (python -V): 3.12.14, also seen on 3.14.7

Model Information:

  • Are you using LiteLLM: Yes
  • Which model is being used: N/A, reproduced with a constructed response

🟡 Optional Information

Additional Context:
_parse_tool_call_arguments promises to raise only json.JSONDecodeError, and both callers catch only that. But json.loads raises a plain ValueError for a huge integer and RecursionError for deep nesting, and the ast.literal_eval repair catches only (SyntaxError, ValueError), so its TypeError escapes. The literal repair can also return values JSON cannot carry. FunctionCall rejects a non-string key, and the streaming path's json.dumps rejects a set or bytes. In streaming, the completion check json.loads("".join(args_parts)) and _JsonPathTracker raise too: its diff recurses per nesting level, and an integer too large for a float fails PartialArg.number_value. On Python 3.14, a tool call written into text whose arguments nest about 40,000 levels decodes and then raises RecursionError when re-encoded in _build_tool_call_from_json_dict. I have a fix with tests ready and will open a PR.

Minimal Reproduction Code:

from google.adk.models.lite_llm import _message_to_generate_content_response
from litellm import ChatCompletionAssistantMessage
from litellm import ChatCompletionMessageToolCall
from litellm import Function

cases = {
    "unhashable key": "{[]: 1}",
    "non-string key": "{1: 2}",
    "huge integer": '{"a": ' + "1" * 5000 + "}",
    "deep nesting": "[" * 100_000 + "]" * 100_000,
}
for label, arguments in cases.items():
  message = ChatCompletionAssistantMessage(
      role="assistant",
      content="Looking that up.",
      tool_calls=[
          ChatCompletionMessageToolCall(
              type="function",
              id="call_1",
              function=Function(name="get_weather", arguments=arguments),
          )
      ],
  )
  try:
    response = _message_to_generate_content_response(message)
    print(f"{label}: {response.finish_reason}")
  except Exception as e:
    print(f"{label}: raised {type(e).__name__}")

How often has this issue occurred?:

  • Once / Rare (it needs a model or provider to return such arguments)

Activity

  1. added a commit that references this issue on Oct 29, 2026
    70819c0
  2. added
    models[Component] This issue is related to model support
    on Oct 29, 2026
  3. surajksharma07 commented on Oct 29, 2026

    @surajksharma07
    Collaborator

    Reproduced all four cases on main 3640e7d @yasumorishima and on the streaming side also seeing the set/bytes and deep-object errors plus {1: 2} coming through as {"1": 2}.

    As a workaround tried converting the extra errors (ValueError, TypeError, RecursionError) from _parse_tool_call_arguments and the streaming json.loads into JSONDecodeError, rejecting repaired args that aren’t plain JSON and guarding _JsonPathTracker.handle_chunk. With that every case came back as MALFORMED_FUNCTION_CALL and valid and single-quoted args still parsed fine.

    Could you check whether your fix in 70819c0 covers the same streaming paths and test it thoroughly against the existing litellm repair tests before you open the PR? Once it’s up will flag it for the maintainers.

  4. surajksharma07 commented on Oct 29, 2026

    @surajksharma07
    Collaborator

    Sorry @yasumorishima, I missed that #7803 was already linked when I posted. Just went through it and it covers the streaming paths I mentioned plus the text and DeepSeek scans with the tests to back it so it looks good from my side.

    One small thing: the key check is top-level only so a nested non-string key like {"a": {1: 2}} still gets turned into a string. If that’s intentional it might be worth a line in the PR description otherwise it’s an easy one to add.

    @xuanyang15 could you take a look at #7803 when you get a chance? It also needs a maintainer to approve the pending workflows. The test_remote_a2a_agent failure in the fork CI also shows up on main so it isn’t from this change.

  5. yasumorishima commented on Oct 30, 2026

    @yasumorishima
    ContributorAuthor

    Thanks for checking it. I'd left nested keys alone because rejecting them changes what main accepts today, but checking only top-level keys is inconsistent: a repaired literal like {'a': {1: 2}} was quietly turned into {"a": {"1": 2}}. The key check now walks the whole value, with tests for a nested dict, a dict inside a list or tuple, and a dict that contains itself.

  6. surajksharma07 commented on Oct 30, 2026

    @surajksharma07
    Collaborator

    Thanks @yasumorishima, went through 71ef7b4 and the walk looks right to me. Since it runs after json.dumps cycles and deep nesting are already ruled out and with the stack instead of recursion it can’t hit the recursion limit itself. The nested dict, list, tuple and self-reference tests cover the cases I had in mind.

    One small thing: the PR description still says “A nested non-string key still becomes a string when encoded, as before” so it’d be worth updating that line to match the new behaviour.

    Also checked the branch against current main and it still merges cleanly. @xuanyang15 gentle ping on #7803 when you get a chance, the 2 workflows are still waiting on maintainer approval.

  7. added a commit that references this issue on Nov 4, 2026
    d5ecb99
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

models[Component] This issue is related to model support

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions