From 3ca82ea880070f71916774df7bdd238ad3291c5a Mon Sep 17 00:00:00 2001 From: axelray-dev <110029405+axelray-dev@users.noreply.github.com> Date: Fri, 26 Jun 2026 03:42:02 +0800 Subject: [PATCH] fix: deduplicate tool call IDs in non-stream parser (#4059) Duplicate tool call ID normalization exists in the streaming parser path but is not shared with the non-stream parser. Non-stream parsing appends raw provider IDs into ToolCallRequest objects without deduplication. Some OpenAI-compatible providers reuse the same tool_call_id for parallel tool calls in non-streaming responses. Without dedup, runner executes both tools with the same ID, producing duplicate tool results with the same tool_call_id, which can fail strict provider validation. Add the same _seen_tc_ids dedup pattern used in _parse_chunks to the _parse method so both paths handle duplicate IDs consistently. --- nanobot/providers/openai_compat_provider.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 91a5b667..7aec961c 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -1131,14 +1131,21 @@ class OpenAICompatProvider(LLMProvider): if reasoning_content is None: reasoning_content = m.get("reasoning_content") + # Deduplicate tool call IDs (same pattern as streaming path) + # Some providers reuse the same ID for parallel tool calls. + _seen_tc_ids: set[str] = set() parsed_tool_calls = [] for tc in raw_tool_calls: tc_map = self._maybe_mapping(tc) or {} fn = self._maybe_mapping(tc_map.get("function")) or {} args = parse_tool_arguments(fn.get("arguments", {})) ec, prov, fn_prov = _extract_tc_extras(tc) + raw_id = str(tc_map.get("id") or _short_tool_id()) + if not raw_id or raw_id in _seen_tc_ids: + raw_id = _short_tool_id() + _seen_tc_ids.add(raw_id) parsed_tool_calls.append(ToolCallRequest( - id=str(tc_map.get("id") or _short_tool_id()), + id=raw_id, name=str(fn.get("name") or ""), arguments=args, extra_content=ec,