From 8e4fe9cfaf9627b5d6955c15ce6b5e094db2b138 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:16:15 +0800 Subject: [PATCH] fix(providers): preserve non-multimodal tool lists --- .../providers/openai_responses/converters.py | 53 ++++++++++++++----- tests/providers/test_openai_responses.py | 14 +++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/nanobot/providers/openai_responses/converters.py b/nanobot/providers/openai_responses/converters.py index b9149d51..d023781f 100644 --- a/nanobot/providers/openai_responses/converters.py +++ b/nanobot/providers/openai_responses/converters.py @@ -99,31 +99,60 @@ def convert_tool_output(content: Any) -> str | list[dict[str, Any]]: converted: list[dict[str, Any]] = [] for item in content: if not isinstance(item, dict): - continue + break item_type = item.get("type") if item_type in {"text", "input_text"}: + if set(item) - {"type", "text", "_meta"}: + break text = item.get("text") - if isinstance(text, str): - converted.append({"type": "input_text", "text": text}) + if not isinstance(text, str): + break + converted.append({"type": "input_text", "text": text}) elif item_type in {"image_url", "input_image"}: image = item.get("image_url") + if isinstance(image, dict) and set(image) - {"url", "detail"}: + break + if set(item) - {"type", "image_url", "file_id", "detail", "_meta"}: + break url = image.get("url") if isinstance(image, dict) else image + file_id = item.get("file_id") + detail = item.get( + "detail", + image.get("detail", "auto") if isinstance(image, dict) else "auto", + ) + if detail not in {"low", "high", "auto", "original"}: + break + block = {"type": "input_image", "detail": detail} if isinstance(url, str) and url: - converted.append({ - "type": "input_image", - "image_url": url, - "detail": item.get("detail", "auto"), - }) + block["image_url"] = url + elif isinstance(file_id, str) and file_id: + block["file_id"] = file_id + else: + break + converted.append(block) elif item_type in {"file", "input_file"}: + if set(item) - { + "type", + "file_data", + "file_id", + "file_url", + "filename", + "_meta", + }: + break block = {"type": "input_file"} for key in ("file_data", "file_id", "file_url", "filename"): value = item.get(key) if isinstance(value, str) and value: block[key] = value - if len(block) > 1: - converted.append(block) - if converted: - return converted + if not any(key in block for key in ("file_data", "file_id", "file_url")): + break + converted.append(block) + else: + break + else: + if converted: + return converted return json.dumps(content, ensure_ascii=False) diff --git a/tests/providers/test_openai_responses.py b/tests/providers/test_openai_responses.py index c36fa098..be615f1a 100644 --- a/tests/providers/test_openai_responses.py +++ b/tests/providers/test_openai_responses.py @@ -283,6 +283,20 @@ class TestConvertMessages: }] assert "_meta" not in str(items[0]) + def test_tool_message_preserves_unknown_list_as_json(self): + content = [ + {"type": "text", "text": "status", "code": 7}, + {"kind": "record", "value": 42}, + ] + + _, items = convert_messages([{ + "role": "tool", + "tool_call_id": "call_1", + "content": content, + }]) + + assert items[0]["output"] == json.dumps(content, ensure_ascii=False) + def test_non_standard_keys_not_leaked(self): """Extra keys on messages must not appear in converted items.""" _, items = convert_messages([{