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
- 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.
- 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.
- Raise generationConfig maxOutputTokens so a long JSON argument body is not truncated mid-string (truncated JSON parses as malformed).
- Switch to a model version with stronger function-calling (latest pro-tier) if you are on an older/smaller variant.
- 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
- Keep tool parameter schemas flat and primitive-heavy; avoid oneOf/anyOf/additionalProperties/$ref constructs from auto-converted zod or JSON-Schema sources.
- Limit how many tools are declared per request; split them across focused sub-agents when the catalog grows.
- Set maxOutputTokens comfortably above the largest JSON argument body the model must emit.
- Log which tool was pending when the error fired so a repeat offender schema is identifiable, and re-run the schema audit whenever tools change.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- NO_FINISH_REASON
- NO_RESPONSE_TEXT
- Failed to create chat object: ${getErrorMessage(e)}
- MAX_TURNS_EXCEEDED
- LOOP_DETECTED
AI-assisted analysis of google-gemini/gemini-cli@3c311beac2 (2026-08-21).
Data as JSON: /api/errors/d913113344f1162a.
Report an issue: GitHub.