google-gemini/gemini-cli · error · InvalidStreamError

MALFORMED_FUNCTION_CALL

MALFORMED_FUNCTION_CALL

Error message

Model stream ended with malformed function call.

What it means

The Gemini API itself reported finishReason MALFORMED_FUNCTION_CALL: the model attempted a tool call but produced arguments that failed validation (typically invalid or truncated JSON), and no valid functionCall part was assembled from the stream. geminiChat.ts:1516 throws immediately whenever this finishReason appears and hasToolCall is false — unlike the text/finishReason checks it is NOT exempted for tool-response turns. It is an InvalidStreamError and is retried internally (with a nudge appended on retry), but if the schema keeps inducing bad arguments the retries fail too.

Source

Thrown at packages/core/src/core/geminiChat.ts:1583

    // Stream validation logic: A stream is considered successful if:
    // 1. There's a tool call OR
    // 2. A not MALFORMED_FUNCTION_CALL finish reason and a non-mepty resp
    //
    // We throw an error only when there's no tool call AND:
    // - No finish reason, OR
    // - MALFORMED_FUNCTION_CALL finish reason OR
    // - Empty response text (e.g., only thoughts with no actual content)
    if (!hasToolCall) {
      if (!finishReason) {
        if (!isOriginalFunctionResponse) {
          throw new InvalidStreamError(
            'Model stream ended without a finish reason.',
            'NO_FINISH_REASON',
          );
        }
      }
      if (finishReason === FinishReason.MALFORMED_FUNCTION_CALL) {
        throw new InvalidStreamError(
          'Model stream ended with malformed function call.',
          'MALFORMED_FUNCTION_CALL',
        );
      }
      if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {
        throw new InvalidStreamError(
          'Model stream ended with unexpected tool call.',
          'UNEXPECTED_TOOL_CALL',
        );
      }
      if (!responseText) {
        if (finishReason === FinishReason.MAX_TOKENS) {
          throw new InvalidStreamError(
            'Model stream ended due to token limit exhaustion (MAX_TOKENS) with empty response text.',
            'MAX_TOKENS_EXCEEDED',
          );
        }
        if (finishReason === FinishReason.SAFETY) {

View on GitHub (pinned to 3c311beac2)

Solutions

  1. Simplify the parameter schema of the failing tool: flatten nested objects, replace unions/oneOf with enum + optional primitives, drop unused optional properties, and keep required lists short.
  2. Reduce the number of tools registered for the turn (or split tool sets across sub-agents) so the model is less likely to garble argument structures.
  3. Raise generationConfig maxOutputTokens so a long JSON argument body is not truncated mid-string (truncated JSON parses as malformed).
  4. Switch to a model version with stronger function-calling (latest pro-tier) if you are on an older/smaller variant.
  5. Retry the turn — a fraction of these are plain model flakiness; the library already retried 3 times, so retry once more after a pause, but do not loop forever on a reproducible schema problem.

Example fix

// before: schema Gemini frequently mangles (unions + deep nesting)
const searchTool = {
  name: 'search',
  parameters: {
    type: 'object',
    properties: {
      filter: {
        oneOf: [
          { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' } } } },
          { type: 'object', properties: { query: { type: 'string', regex: '.+' } } },
        ],
      },
      options: { type: 'object', properties: { paging: { type: 'object', properties: { offset: { type: 'number' }, limit: { type: 'number' } } } } },
    },
  },
};

// after: flattened, Gemini-function-calling-friendly
const searchTool = {
  name: 'search',
  parameters: {
    type: 'object',
    properties: {
      filter_ids:   { type: 'array', items: { type: 'string' }, description: 'Match these ids' },
      filter_query: { type: 'string', description: 'Free-text query' },
      offset:       { type: 'number' },
      limit:        { type: 'number' },
    },
  },
};
Defensive patterns

Strategy: retry

Validate before calling

// Run BEFORE registering tools: flag schema constructs Gemini's function calling
// frequently mangles into MALFORMED_FUNCTION_CALL.
function auditToolSchemas(tools: Array<{ name: string; parameters: unknown }>): string[] {
  const problems: string[] = [];
  const walk = (node: any, path: string) => {
    if (!node || typeof node !== 'object') return;
    for (const kw of ['oneOf', 'anyOf', 'allOf', 'not', 'additionalProperties']) {
      if (kw in node) problems.push(`${path}: uses ${kw}`);
    }
    if (path.split('.').length > 4) problems.push(`${path}: nested deeper than 4 levels`);
    for (const [k, v] of Object.entries(node.properties ?? {})) walk(v, `${path}.${k}`);
    if (Array.isArray(node.required) && node.required.length > 6) problems.push(`${path}: ${node.required.length} required fields`);
  };
  for (const t of tools) walk(t.parameters, t.name);
  return problems; // non-empty => simplify before sending, or you will retry forever
}

Type guard

import { InvalidStreamError } from './packages/core/src/core/geminiChat.js';

function isMalformedFunctionCallError(e: unknown): e is InvalidStreamError & { type: 'MALFORMED_FUNCTION_CALL' } {
  return e instanceof InvalidStreamError && e.type === 'MALFORMED_FUNCTION_CALL';
}

Try / catch

try {
  for await (const chunk of chat.sendMessageStream(userMsg)) { handle(chunk); }
} catch (e) {
  if (isMalformedFunctionCallError(e)) {
    // One retry is reasonable (model flakiness); a second consecutive failure means
    // the schema itself is the problem — simplify the tool instead of looping.
    if (++malformedCount === 1) return runTurn(chat, userMsg);
    throw new Error('Tool schema induces malformed function calls; simplify it: ' + lastToolName);
  }
  throw e;
}

Prevention

When it happens

Trigger: A streaming generateContent request with tool declarations where the model emits a function call whose arguments are unparseable, so the final chunk carries finishReason=MALFORMED_FUNCTION_CALL instead of a usable functionCall part. Strongly correlated with: tool parameter schemas using constructs outside Gemini's function-calling subset (oneOf/anyOf/allOf/not, additionalProperties, deeply nested objects, exotic formats), very large schemas, dozens of concurrently declared tools (the model mixes up argument shapes), and maxOutputTokens too small to fit the JSON arguments.

Common situations: You just added a new tool with a big auto-generated (e.g., zod-to-JSON-Schema) schema and now calls intermittently fail; LangChain/JSON-schema converted types with $refs and unions that Gemini cannot express; intermittent failures on one specific tool out of many while others work; after switching to a smaller/older model with weaker function calling; failures that cluster when the model must produce very long argument strings.

Understand the failure class

Related errors


AI-assisted analysis of google-gemini/gemini-cli@3c311beac2 (2026-08-21). Data as JSON: /api/errors/d913113344f1162a. Report an issue: GitHub.