Skip to content

Conversation

@emerzon
Copy link
Contributor

@emerzon emerzon commented Feb 8, 2026

Relevant issues

Fixes #20699

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🐛 Bug Fix
✅ Test

Changes

  • Fixed _ensure_tool_results_have_corresponding_tool_calls to correctly handle cached tool-call values that are object-like (for example ChatCompletionMessageToolCall) instead of blindly requiring a dict.
  • Added _normalize_tool_use_definition(...) to normalize cached tool-call payloads before creating tool call chunks.
  • Updated _create_tool_call_chunk(...) to support object-like function payloads (e.g. Function) and preserve name / arguments.
  • Added a regression test in tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py to cover cache values stored as ChatCompletionMessageToolCall objects.

Root cause

transform_chat_completion_tools_to_responses_tools writes ChatCompletionMessageToolCall objects into TOOL_CALLS_CACHE, but _ensure_tool_results_have_corresponding_tool_calls previously treated any non-dict cache hit as invalid and replaced it with {}. That dropped tool metadata and injected malformed tool calls (name='', arguments='{}').

Validation

Executed locally:

  • poetry run pytest -q tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py
  • poetry run pytest -q tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py::TestFunctionCallTransformation::test_ensure_tool_results_preserves_cached_openai_object_tool_call tests/llm_responses_api_testing/test_anthropic_tool_result_fix.py::test_fix_ensures_tool_calls_for_tool_results

Also re-ran the issue's reproduction flow and verified the recovered tool call now preserves:

  • function.name == "search_web"
  • function.arguments == '{"query": "python bugs"}'

Copilot AI review requested due to automatic review settings February 8, 2026 10:58
@vercel
Copy link

vercel bot commented Feb 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 8, 2026 11:12pm

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 8, 2026

Greptile Overview

Greptile Summary

This PR fixes tool-call recovery in the Responses→ChatCompletions transformation path by normalizing cached tool-call entries before synthesizing missing tool_use blocks. It adds _normalize_tool_use_definition(...), updates _create_tool_call_chunk(...) to preserve function.name/arguments when cached values are object-like, and introduces a regression test covering a cached ChatCompletionMessageToolCall entry.

This fits into LiteLLMCompletionResponsesConfig._ensure_tool_results_have_corresponding_tool_calls, which must ensure Anthropic-compatible message structure by backfilling tool_calls on the prior assistant message when a tool role message appears without a corresponding tool_use.

Confidence Score: 3/5

  • Mostly safe, but the main fix may not work for attribute-based cached tool-call objects, which can still result in dropped tool metadata during recovery.
  • Change is localized and adds a regression test, but both normalization and chunk creation only recognize mapping-like objects via .get. If cached tool calls/functions are Pydantic/typed models without .get, the intended preservation of name/arguments will not occur and the original bug can persist.
  • litellm/responses/litellm_completion_transformation/transformation.py

Important Files Changed

Filename Overview
litellm/responses/litellm_completion_transformation/transformation.py Adds normalization for cached tool_call entries and extends tool-call chunk creation to accept mapping-like objects; current implementation only supports .get-style objects, so attribute-based Pydantic models may still drop function.name/arguments during recovery.
tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py Adds regression test that caches a ChatCompletionMessageToolCall and asserts recovered tool call preserves function.name/arguments; test may be brittle if those types don’t implement .get in practice.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant Ensure as _ensure_tool_results_have_corresponding_tool_calls
    participant Cache as TOOL_CALLS_CACHE
    participant Norm as _normalize_tool_use_definition
    participant Chunk as _create_tool_call_chunk
    participant Asst as Previous assistant msg

    Caller->>Ensure: messages, tools?
    loop each message[i]
        Ensure->>Ensure: if role != "tool" skip
        Ensure->>Ensure: find prev_assistant_idx
        Ensure->>Ensure: recover tool_call_id if missing
        alt tool_call_id missing and removable
            Ensure->>Ensure: mark tool message for removal
        else tool_call_id present
            Ensure->>Asst: _get_tool_calls_list(prev_assistant)
            Ensure->>Ensure: _check_tool_call_exists?
            alt missing tool_call
                Ensure->>Cache: get_cache(tool_call_id)
                alt cache miss and tools provided
                    Ensure->>Ensure: _reconstruct_tool_call_from_tools
                end
                Ensure->>Norm: normalize(cached_def, tool_call_id)
                alt normalized_def present
                    Ensure->>Chunk: create chunk(normalized_def)
                    Ensure->>Asst: append tool_call_chunk
                else normalization failed
                    Ensure-->>Ensure: cannot recover tool call
                end
            end
        end
    end
    Ensure-->>Caller: fixed_messages (with tool_calls added/removals)
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 612 to 614
function: Dict[str, Any] = function_raw
elif hasattr(function_raw, "get"):
function = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object .get assumption
_create_tool_call_chunk treats any function_raw with a .get attribute as mapping-like and calls function_raw.get(...). If Function is a Pydantic model (common in this repo), it typically does not implement .get, so this branch won’t run and you’ll fall back to {}, losing name/arguments again. Consider handling attribute-based objects too (e.g. hasattr(function_raw, "name")/"arguments") or normalizing function inside _normalize_tool_use_definition to a plain dict.

Comment on lines +642 to +644
"""
Normalize cached tool_call definitions to a dict-like shape consumed by _create_tool_call_chunk.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalization misses attr objects
_normalize_tool_use_definition only supports dict or objects exposing .get. If the cache stores a typed ChatCompletionMessageToolCall/Function as an attribute-based model (no .get), this will return None and tool call recovery won’t happen. To match the PR intent (“object-like”), add an attribute-based path (e.g. getattr(tool_use_definition, "id", None), etc.).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Responses API multi-turn tool-calling recovery bug where cached tool call objects (e.g., ChatCompletionMessageToolCall) were previously treated as invalid because they weren’t dicts, causing recovered tool calls to lose function.name / function.arguments and become malformed.

Changes:

  • Update tool-call recovery to normalize cached tool-call objects into a dict-like shape before chunk creation.
  • Extend _create_tool_call_chunk(...) to support object-like function payloads via .get(...) accessors.
  • Add a regression test to ensure cached ChatCompletionMessageToolCall objects preserve function metadata during recovery.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
litellm/responses/litellm_completion_transformation/transformation.py Normalizes cached tool-call entries and preserves tool call metadata when reconstructing missing assistant tool_calls.
tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py Adds regression test covering cached OpenAI-object tool calls in TOOL_CALLS_CACHE.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@emerzon
Copy link
Contributor Author

emerzon commented Feb 8, 2026

@greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 8, 2026

Greptile Overview

Greptile Summary

This PR fixes a critical bug where cached ChatCompletionMessageToolCall objects were being discarded during tool result recovery, causing malformed tool calls with empty names and arguments.

Key Changes:

  • Added _get_mapping_or_attr_value helper that safely extracts values from dicts, dict-like objects (with .get()), and plain attribute-based objects
  • Added _normalize_tool_use_definition to convert cached tool call objects (regardless of type) into normalized dicts before processing
  • Updated _create_tool_call_chunk to use the new helper for safe field extraction
  • Replaced the previous logic that blindly converted non-dict cached values to {} (losing all metadata)

Root Cause:
The transform_chat_completion_tools_to_responses_tools function stores ChatCompletionMessageToolCall objects (Pydantic models) in TOOL_CALLS_CACHE, but _ensure_tool_results_have_corresponding_tool_calls previously treated any non-dict as invalid and replaced it with an empty dict, causing data loss.

Testing:

  • Added test for ChatCompletionMessageToolCall (Pydantic-based) objects
  • Added test for plain attribute-based objects
  • Both tests verify that function.name and function.arguments are correctly preserved

Minor observations:

  • _get_mapping_or_attr_value uses broad except Exception at line 620 which could silently mask real errors
  • Redundant default value handling in _create_tool_call_chunk (lines 640-641 and 659-660)

Confidence Score: 4/5

  • This PR is safe to merge with low risk - it fixes a real bug with proper test coverage
  • Score reflects a solid bug fix with comprehensive testing. The implementation correctly handles both Pydantic models and plain objects through the _get_mapping_or_attr_value helper with appropriate fallback logic. Tests cover both ChatCompletionMessageToolCall objects and custom attribute-based objects. Minor concerns about broad exception handling and redundant default logic prevent a perfect score, but these don't affect correctness.
  • No files require special attention - the changes are focused and well-tested

Important Files Changed

Filename Overview
litellm/responses/litellm_completion_transformation/transformation.py Adds robust handling for cached tool call objects (both dict-like and attribute-based) through new helper methods _get_mapping_or_attr_value and _normalize_tool_use_definition, fixing data loss when recovering tool calls from cache
tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py Adds comprehensive test coverage for both ChatCompletionMessageToolCall (Pydantic) objects and plain attribute-based objects, validating that cached tool call metadata is properly preserved during recovery

Sequence Diagram

sequenceDiagram
    participant Cache as TOOL_CALLS_CACHE
    participant Recovery as Recovery Function
    participant Normalize as Normalize Function
    participant Helper as Helper Function
    participant CreateChunk as Create Chunk Function
    participant Message as Assistant Message

    Note over Recovery: Detect tool result without tool call
    Recovery->>Cache: get cached tool call
    Cache-->>Recovery: ChatCompletionMessageToolCall
    
    Recovery->>Normalize: normalize cached object
    
    alt Object is dict
        Normalize->>Normalize: use dict directly
    else Object is attribute-based
        Normalize->>Helper: get id attribute
        Helper-->>Normalize: return id value
        Normalize->>Helper: get type attribute
        Helper-->>Normalize: return type value
        Normalize->>Helper: get function name
        Helper-->>Normalize: return name value
        Normalize->>Helper: get function arguments
        Helper-->>Normalize: return arguments value
        Normalize->>Normalize: build normalized dict
    end
    
    Normalize-->>Recovery: normalized tool definition
    
    Recovery->>CreateChunk: create tool call chunk
    CreateChunk->>Helper: extract fields safely
    Helper-->>CreateChunk: field values
    CreateChunk-->>Recovery: ChatCompletionToolCallChunk
    
    Recovery->>Message: add recovered tool call
    Message-->>Recovery: updated message
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

…tion.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@emerzon
Copy link
Contributor Author

emerzon commented Feb 9, 2026

@greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR fixes tool-call recovery for the Responses API transformation when tool results arrive without a matching tool_use/tool_calls entry. It does this by normalizing cached tool-call payloads (including object-like values such as ChatCompletionMessageToolCall / function payload objects) before creating ChatCompletionToolCallChunks, and adds regression tests to ensure cached object tool calls preserve function.name and function.arguments.

The core flow lives in LiteLLMCompletionResponsesConfig._ensure_tool_results_have_corresponding_tool_calls, which looks up missing tool calls in TOOL_CALLS_CACHE (or reconstructs from tools) and then injects a synthesized tool_call into the previous assistant message so downstream providers (notably Anthropic) accept the message sequence.

Confidence Score: 4/5

  • Mostly safe to merge, but one runtime type mismatch can crash tool-result recovery for object-style messages.
  • The normalization changes and added tests align with the stated bugfix and should improve correctness for cached tool-call objects. However, _ensure_tool_results_have_corresponding_tool_calls still uses .get() on items typed as possibly non-dict message objects, which will raise at runtime in those scenarios and block the intended recovery path.
  • litellm/responses/litellm_completion_transformation/transformation.py

Important Files Changed

Filename Overview
litellm/responses/litellm_completion_transformation/transformation.py Adds normalization/helpers to preserve cached tool call objects when reconstructing tool_calls; however _ensure_tool_results_have_corresponding_tool_calls still assumes dict messages via .get() in role checks, which will crash for object-style messages allowed by its type signature.
tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py Adds regression tests covering cached tool calls stored as OpenAI tool-call objects and as attribute-only objects, ensuring tool call recovery preserves function name/arguments.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Transform as LiteLLMCompletionResponsesConfig
    participant Cache as TOOL_CALLS_CACHE
    participant Assistant as prev assistant msg

    Caller->>Transform: _ensure_tool_results_have_corresponding_tool_calls(messages, tools)
    loop for each tool message
        Transform->>Transform: _find_previous_assistant_idx()
        Transform->>Transform: _get_tool_calls_list(prev_assistant)
        alt missing tool_call_id in prev assistant
            Transform->>Cache: get_cache(tool_call_id)
            alt cache miss and tools provided
                Transform->>Transform: _reconstruct_tool_call_from_tools(tool_call_id, tools)
            end
            Transform->>Transform: _normalize_tool_use_definition(cached_or_reconstructed, tool_call_id)
            Transform->>Transform: _create_tool_call_chunk(normalized, tool_call_id, index)
            Transform->>Assistant: _add_tool_call_to_assistant(prev_assistant, chunk)
        end
    end
    Transform-->>Caller: fixed_messages
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 9, 2026

Additional Comments (1)

litellm/responses/litellm_completion_transformation/transformation.py
Non-dict messages crash
_ensure_tool_results_have_corresponding_tool_calls deep-copies messages but then unconditionally does msg.get("role") (and later message.get("role")) when counting/iterating. Since messages is typed as Union[..., ChatCompletionResponseMessage], passing object-style messages (no .get) will raise AttributeError before any tool call recovery runs. Consider using the existing dict/attr pattern used elsewhere in this function (e.g., isinstance(msg, dict) / getattr(msg, "role", None)) for role access.

Also appears at transformation.py:774-775.

@krrishdholakia krrishdholakia changed the base branch from main to litellm_oss_staging_02_09_2026 February 10, 2026 04:08
@krrishdholakia krrishdholakia merged commit 11531da into BerriAI:litellm_oss_staging_02_09_2026 Feb 10, 2026
7 of 8 checks passed
Sameerlite pushed a commit that referenced this pull request Feb 10, 2026
…very (#20700)

* fix(responses): preserve cached tool call objects in tool result recovery

* fix(responses): support attr-based cached tool call recovery

* Update litellm/responses/litellm_completion_transformation/transformation.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Responses API Tool Cache Type Mismatch Corrupts Multi-Turn Tool Calling

2 participants