◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 8 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 8 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-09-22Advanced

When Function Calls Turn Into Plain Text, Suspect the Property Names in Your Tool Declarations

A record of chasing MALFORMED_FUNCTION_CALL after output limits and schema conflicts were ruled out, bisecting nine tool declarations down to one property name, and adding a linter that stops bad names before they ship.

Gemini API249function calling4MALFORMED_FUNCTION_CALLschema designPython51

✦ Premium Article

I added one small tool to the classification pipeline behind my ukiyo-e wallpaper app, and the next morning calls that had worked all week came back with finishReason: MALFORMED_FUNCTION_CALL. The new tool only returned a colour histogram. I had not touched a single line of the existing declarations.

My first suspicion was the output token limit. My second was a conflict between the schema and the natural-language instructions. Both were wrong — doubling the limit changed nothing, and emptying the instruction text changed nothing either. The calls kept collapsing into prose at exactly the same point.

What it came down to was neither the description nor the argument type of the new tool. It was the name of its property. One trailing underscore, and the problem stopped that same night. Here is how I narrowed it down, and the line I drew afterwards.

What Came Back Was Not an Error, It Was Prose

MALFORMED_FUNCTION_CALL is not an HTTP error. The status is 200 and only the body is broken, which is part of why it took me so long to look in the right place.

What actually came back looked like this.

{
  "candidates": [
    {
      "content": {
        "parts": [
          { "text": "print(default_api.classify_palette(in=\"new/2026-09/ukiyoe-0431.jpg\"))" }
        ],
        "role": "model"
      },
      "finishReason": "MALFORMED_FUNCTION_CALL"
    }
  ]
}

There is no functionCall in parts. Instead there is a function call written out as characters in a text part. The model tried to call a tool, could not finish assembling the call, and fell back into writing prose about it.

The part that mattered most: classify_palette was not the only casualty. The two other tools that should have fired in the same turn came back as prose too. One broken declaration takes the whole turn with it.

One declaration is broken, but every call in that turn shows the symptom. That is exactly why starting from "the tool I added last" leads you astray.

Rule Out the Output Limit and the Instruction Conflict First

Before you start suspecting names, clear the two well-known causes out of the way. Both take a few minutes, and skipping the order costs more time than it saves.

Checking the limit

Raise maxOutputTokens generously and send the same input again. If the limit was the cause, you get STOP back here. If you do not, the limit is irrelevant. On models that spend a thinking budget, that budget comes out of the same allowance, so leave real headroom when you test.

Checking the instructions

Temporarily strip anything from the system instruction and user input that constrains the output shape — return JSON, wrap it in a code block, and so on. Structured output and tool declarations do not always sit well together. That layer is the same territory I covered in When responseSchema Ignores Your enum in the Gemini API.

Once those two were out, I accepted that the problem lived inside the declarations themselves. That is where this gets interesting.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to narrow nine tool declarations down to the single guilty one in four API calls, instead of guessing your way through them
✦You will have a short list of names you refuse to put in a schema, so the day you add one more tool does not quietly break the calls that worked yesterday
✦You will stop treating MALFORMED_FUNCTION_CALL as something that just happens, and separate it into three layers: output limits, conflicting instructions, and the names in your declarations
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.
◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.
◈ API / SDK2026-09-12
gemini-2.5-flash-image stops on October 2, and the replacement the table names retired in June
gemini-2.5-flash-image shuts down on October 2, 2026, but the recommended replacement listed in the official table, gemini-3.1-flash-image-preview, was already retired on June 25. Here is the script I wrote to follow replacement chains to their end, and what it found across every row of the table.