Skip to content

ChatCompletionCache cache key ignores tool_choice, serves stale cached response when only tool_choice differs #7968

Description

@ErenAta16

What happened?

ChatCompletionCache's cache key doesn't include tool_choice. Two calls with identical messages/tools/json_output/extra_create_args but different tool_choice collide on the same cache key, so the second call silently gets served a stale, wrong-shaped cached response instead of hitting the underlying client.

tool_choice is a first-class parameter of ChatCompletionClient.create/create_stream (auto lets the model decide, required forces a tool call, none forbids tool calls, a specific Tool forces that exact tool), and it materially changes the shape of the response, in the real OpenAI client it's translated directly into the API's tool_choice field and changes whether the response is plain text or a list of FunctionCalls.

ChatCompletionCache._check_cache (python/packages/autogen-ext/src/autogen_ext/models/cache/_chat_completion_cache.py) builds its hash from messages, tools, json_output, and extra_create_args only:

data = {
    "messages": [message.model_dump() for message in messages],
    "tools": [(tool.schema if isinstance(tool, Tool) else tool) for tool in tools],
    "json_output": json_output_data,
    "extra_create_args": extra_create_args,
}
serialized_data = json.dumps(data, sort_keys=True)
cache_key = hashlib.sha256(serialized_data.encode()).hexdigest()

create() calls self._check_cache(messages, tools, json_output, extra_create_args), no tool_choice, even though it's an explicit parameter of create() itself and gets correctly forwarded to the underlying client a few lines later on cache miss. create_stream() has the identical gap.

This is exactly the scenario the codebase itself exercises internally: AssistantAgent._reflect_on_tool_use_flow explicitly passes tool_choice="none" to force a plain-text reflection response after tool execution, while the normal tool-call path uses the default tool_choice="auto". Any application/agent design that reuses one ChatCompletionCache-wrapped client across calls that vary only tool_choice for the same conversation prefix will silently get the wrong kind of answer.

What did you expect to happen?

A call that only differs from a previous one by tool_choice should be a cache miss and actually invoke the underlying client, not replay a response computed for a different tool_choice.

How can we reproduce it (as minimally and precisely as possible)?

import asyncio
from autogen_core import InMemoryStore
from autogen_core.models import UserMessage
from autogen_ext.models.cache import ChatCompletionCache
from autogen_ext.models.replay import ReplayChatCompletionClient

async def main():
    responses = ["This is dummy message number 0", "This is dummy message number 1"]
    replay_client = ReplayChatCompletionClient(responses)
    replay_client.set_cached_bool_value(False)
    cached_client = ChatCompletionCache(replay_client, InMemoryStore())

    message = [UserMessage(content="hi", source="user")]

    r0 = await cached_client.create(message, tool_choice="auto")
    print("call 1 (tool_choice=auto):", r0.cached, repr(r0.content))

    r1 = await cached_client.create(message, tool_choice="none")
    print("call 2 (tool_choice=none):", r1.cached, repr(r1.content))

asyncio.run(main())

Output:

call 1 (tool_choice=auto): False 'This is dummy message number 0'
call 2 (tool_choice=none): True 'This is dummy message number 0'

Call 2 reports cached=True and returns call 1's response, even though it was made with a different tool_choice and, with a real tool_choice-aware client, should produce a genuinely different (plain-text) response. I also reproduced this with a fake client whose response actually depends on tool_choice (mirroring a real provider), confirming the underlying client is never invoked for the second call at all (only tool_choice='auto' shows up in the fake client's call log).

AutoGen version

Reproduced against the current main branch source of _chat_completion_cache.py, using the installed autogen-ext/autogen-core packages from an editable install of the same source. Python 3.12.10, Windows.

Which package was this bug in?

Python autogen-ext

Model used

N/A, reproduced with ReplayChatCompletionClient and a fake client, no real model API needed.

Python version

3.12

Operating system

Windows

Additional Information

Opening a fix, will link the PR here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions