diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index b8112b529..03ab35a0e 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -1097,6 +1097,15 @@ class OpenAICompatProvider(LLMProvider): if delta: _accum_legacy_function_call(getattr(delta, "function_call", None)) + # Some providers (e.g. Zhipu/GLM) reuse the same tool_call id for + # parallel tool calls in streaming mode. Deduplicate before building + # the response so downstream tool messages don't collide. + _seen_tc_ids: set[str] = set() + for b in tc_bufs.values(): + if not b["id"] or b["id"] in _seen_tc_ids: + b["id"] = _short_tool_id() + _seen_tc_ids.add(b["id"]) + return LLMResponse( content="".join(content_parts) or None, tool_calls=[ diff --git a/nanobot/utils/file_edit_events.py b/nanobot/utils/file_edit_events.py index b5d2f6d73..ff2594435 100644 --- a/nanobot/utils/file_edit_events.py +++ b/nanobot/utils/file_edit_events.py @@ -367,12 +367,14 @@ class StreamingFileEditTracker: def apply_final_call_ids(self, final_tool_calls: list[Any]) -> None: """Keep final start/end events keyed to any earlier streamed placeholder.""" + used_canonicals: set[str] = set() for tool_call in final_tool_calls: canonical = self.canonical_call_id_for(tool_call) - if canonical: + if canonical and canonical not in used_canonicals: try: tool_call.id = canonical - except Exception: + used_canonicals.add(canonical) + except (AttributeError, TypeError): pass def canonical_call_id_for(self, tool_call: Any) -> str | None: