Duplicate same-name parallel tool calls collapse to a single execution #1431 #1457
Replies: 5 comments 5 replies
|
PR #1435 is opened. Is this good? This changed tool_calls from dict to list instead of set because of the follow reason
|
|
My biggest worry is that by "changing" the tool "name" the model we return its result to will be confused. I do believe the pre-pend an id solution works well for the actual execution part of the problem, but I'm unclear if it will work for the return the results to the model part (which I assume is the intent of doing tool calls) |
|
I think the component prefixing is a separate question. There's two scenarios I think we should address with this:
It looks like a list is the best way to handle this. Some providers (like openai) also append a call_id to tool requests. We might be able to use that to prevent deduplication as well. |
I see, I was under the misunderstanding the first scenario was already addressed in our code and focused entirely on the second. And in my recent PR I surfaced |
|
The discussion is focus on the ModelToolCall. It is created when the model generates tool_call response. It is saved into a dict with the function name as a key. The function name is given to the model in Tool object in the request. It should be unique (#1432 should fix) but when the model needs to execute the same function multiple time, only one ToolCall request stays the dict because the function name is the key. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
When a model requests the same tool more than once in a single response (parallel tool calling — e.g. search({"q": "X"}) and search({"q": "Y"}) in one turn), Mellea executes only the last call and silently drops the rest.
Root cause
Model tool calls are stored in a dict keyed by tool name:
ModelOutputThunk.tool_calls: dict[str, ModelToolCall] (mellea/core/base.py)
Populated per backend via model_tool_calls[tool_name] = ModelToolCall(...):
mellea/helpers/openai_compatible_helpers.py (OpenAI-compatible, LiteLLM, Watsonx)
mellea/backends/ollama.py
Because the key is the tool name, a second call to the same name overwrites the first before execution. _acall_tools (mellea/stdlib/functional.py) then iterates the collapsed dict, so the tool runs once and produces a single ToolMessage. The dropped call's result never returns to the model.
This dates to the initial commit and predates parallel tool calling being common; the "one call per tool name per turn" assumption is no longer valid for current OpenAI/Anthropic-style APIs.
Impact
Correctness (primary): parallel calls to the same tool with different arguments lose all but the last. Affects tool execution and the messages fed back to the model — not just observability.
Observability (symptom): the mellea.tool.calls metric and the execute_tool span each fire once instead of N times. This faithfully reflects that only one call executed — the telemetry is correct about a broken execution. No telemetry code needs to change.
Suggested fix
Key tool calls by the provider-supplied call id rather than by name. ModelToolCall.tool_call_id (added in #1430) is the field a fix would key on. This is a data-model change (dict[str, ModelToolCall] → keyed by call id, or a list) touching core/base.py and all four backend extraction sites; no telemetry code needs to change — the correct span/metric cardinality falls out automatically once execution stops collapsing.
Related
Surfaced by the execute_tool span work (#1430), which added ModelToolCall.tool_call_id but deliberately does not attempt the data-model change.
Adjacent tool-call serialization work: #1389 (result-turn tool_call_id feedback).
All reactions