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.
What happened?
ChatCompletionCache's cache key doesn't includetool_choice. Two calls with identicalmessages/tools/json_output/extra_create_argsbut differenttool_choicecollide 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_choiceis a first-class parameter ofChatCompletionClient.create/create_stream(autolets the model decide,requiredforces a tool call,noneforbids tool calls, a specificToolforces that exact tool), and it materially changes the shape of the response, in the real OpenAI client it's translated directly into the API'stool_choicefield and changes whether the response is plain text or a list ofFunctionCalls.ChatCompletionCache._check_cache(python/packages/autogen-ext/src/autogen_ext/models/cache/_chat_completion_cache.py) builds its hash frommessages,tools,json_output, andextra_create_argsonly:create()callsself._check_cache(messages, tools, json_output, extra_create_args), notool_choice, even though it's an explicit parameter ofcreate()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_flowexplicitly passestool_choice="none"to force a plain-text reflection response after tool execution, while the normal tool-call path uses the defaulttool_choice="auto". Any application/agent design that reuses oneChatCompletionCache-wrapped client across calls that vary onlytool_choicefor 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_choiceshould be a cache miss and actually invoke the underlying client, not replay a response computed for a differenttool_choice.How can we reproduce it (as minimally and precisely as possible)?
Output:
Call 2 reports
cached=Trueand returns call 1's response, even though it was made with a differenttool_choiceand, 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 ontool_choice(mirroring a real provider), confirming the underlying client is never invoked for the second call at all (onlytool_choice='auto'shows up in the fake client's call log).AutoGen version
Reproduced against the current
mainbranch source of_chat_completion_cache.py, using the installedautogen-ext/autogen-corepackages 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
ReplayChatCompletionClientand 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.