{"record":{"id":"95e226d8a343e45e","repo":"BerriAI/litellm","slug":"missing-corresponding-tool-call-for-tool-response","errorCode":null,"errorMessage":"Missing corresponding tool call for tool response message. Received - message={message}, last_message_with_tool_calls={last_message_with_tool_calls}","messagePattern":"Missing corresponding tool call for tool response message\\. Received - message=(.+?), last_message_with_tool_calls=(.+?)","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/prompt_templates/factory.py","lineNumber":1497,"sourceCode":"    if last_message_with_tool_calls:\n        tools: Final = last_message_with_tool_calls.get(\"tool_calls\", [])\n        msg_tool_call_id: Final = message.get(\"tool_call_id\", None)\n        for tool in tools:\n            prev_tool_call_id = tool.get(\"id\", None)\n            if msg_tool_call_id and prev_tool_call_id and msg_tool_call_id == prev_tool_call_id:\n                name = tool.get(\"function\", {}).get(\"name\", \"\")\n\n    # Echo the OpenAI tool_call_id on functionResponse (strip thought-signature suffix).\n    gemini_call_id: str | None = None\n    if forward_function_call_id:\n        raw_tool_call_id: Final = message.get(\"tool_call_id\")\n        if raw_tool_call_id and isinstance(raw_tool_call_id, str):\n            stripped_id: Final = raw_tool_call_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1)[0]\n            if stripped_id:\n                gemini_call_id = stripped_id\n\n    if not name:\n        raise Exception(\n            f\"Missing corresponding tool call for tool response message. Received - message={message}, last_message_with_tool_calls={last_message_with_tool_calls}\"\n        )\n\n    # Parse response data - support both JSON string and plain string\n    # For Computer Use, the response should contain structured data like {\"url\": \"...\"}\n    response_data: dict\n    try:\n        if content_str.strip().startswith(\"{\") or content_str.strip().startswith(\"[\"):\n            # Try to parse as JSON (for Computer Use structured responses)\n            parsed: Final = json.loads(content_str)\n            if isinstance(parsed, dict):\n                response_data = parsed  # Use the parsed JSON directly\n            else:\n                response_data = {\"content\": content_str}\n        else:\n            response_data = {\"content\": content_str}\n    except (json.JSONDecodeError, ValueError):\n        # Not valid JSON, wrap in content field","sourceCodeStart":1479,"sourceCodeEnd":1515,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/prompt_templates/factory.py#L1479-L1515","documentation":"Raised when converting a role='tool' message for Gemini: LiteLLM looks up the function name by matching message.tool_call_id against the tool_calls in the last assistant message with tool calls, and no match with a non-empty name was found. Gemini needs the function name on every functionResponse, so an unmatched tool result cannot be forwarded.","triggerScenarios":"Sending a tool message whose tool_call_id doesn't match any id in the preceding assistant tool_calls (typos, regenerated ids, stripped thought-signature suffix mismatch); sending the tool response without the assistant tool-call message in between; empty tool_calls in the referenced assistant message.","commonSituations":"Multi-turn agent loops where history is trimmed and the assistant tool-call turn falls out of the window; parallel tool calls where results are matched by index instead of id; providers that mutate tool_call ids; replaying stored conversations after id sanitization.","solutions":["Ensure the assistant message containing the matching tool_calls (with ids) is present immediately before the tool result in messages","Echo back the exact tool_call_id from the assistant message on the tool response","When trimming history, never cut between an assistant tool_calls turn and its tool responses","For parallel calls, keep every tool response, one per tool_call_id, before the next user turn"],"exampleFix":"# before\nmessages = [\n    {\"role\": \"user\", \"content\": \"Weather?\"},\n    {\"role\": \"tool\", \"tool_call_id\": \"call_1\", \"content\": \"72F\"},  # assistant tool_calls turn was trimmed\n]\n\n# after\nmessages = [\n    {\"role\": \"user\", \"content\": \"Weather?\"},\n    {\"role\": \"assistant\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\",\n        \"function\": {\"name\": \"get_weather\", \"arguments\": \"{\\\"city\\\": \\\"SF\\\"}\"}}]},\n    {\"role\": \"tool\", \"tool_call_id\": \"call_1\", \"content\": \"72F\"},\n]","handlingStrategy":"validation","validationCode":"def tool_results_match(messages) -> bool:\n    last_ids = None\n    for m in messages:\n        if m.get(\"role\") == \"assistant\" and m.get(\"tool_calls\"):\n            last_ids = {tc.get(\"id\") for tc in m[\"tool_calls\"]}\n        elif m.get(\"role\") == \"tool\":\n            if not last_ids or m.get(\"tool_call_id\") not in last_ids:\n                return False\n    return True\n\nassert tool_results_match(messages), \"tool result without matching assistant tool_call\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Echo tool_call_id exactly from the assistant tool_call","Keep assistant tool_calls turns adjacent to their tool responses when trimming history","One tool response per tool_call_id, all before the next user turn"],"tags":["gemini","tool-calling","tool-result","history"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}