fix: use json.dumps instead of str() for Codex Responses API arguments

When the Responses API returns tool call arguments as a dict,
str(dict) produces Python repr with single quotes (e.g. {'key': 'val'})
which is invalid JSON. Downstream json.loads() fails silently and the
tool gets called with empty arguments, losing all parameters.

Affects both function_call and custom_tool_call item types in
_normalize_codex_response().
This commit is contained in:
0xbyt4
2026-03-13 00:01:14 +03:00
committed by teknium1
parent 6761021fb4
commit 6f85283553
2 changed files with 55 additions and 2 deletions

View File

@@ -2407,7 +2407,7 @@ class AIAgent:
fn_name = getattr(item, "name", "") or ""
arguments = getattr(item, "arguments", "{}")
if not isinstance(arguments, str):
arguments = str(arguments)
arguments = json.dumps(arguments, ensure_ascii=False)
raw_call_id = getattr(item, "call_id", None)
raw_item_id = getattr(item, "id", None)
embedded_call_id, _ = self._split_responses_tool_id(raw_item_id)
@@ -2428,7 +2428,7 @@ class AIAgent:
fn_name = getattr(item, "name", "") or ""
arguments = getattr(item, "input", "{}")
if not isinstance(arguments, str):
arguments = str(arguments)
arguments = json.dumps(arguments, ensure_ascii=False)
raw_call_id = getattr(item, "call_id", None)
raw_item_id = getattr(item, "id", None)
embedded_call_id, _ = self._split_responses_tool_id(raw_item_id)

View File

@@ -2533,3 +2533,56 @@ class TestVprintForceOnErrors:
agent._vprint("debug")
agent._vprint("error", force=True)
assert len(printed) == 2
class TestNormalizeCodexDictArguments:
"""_normalize_codex_response must produce valid JSON strings for tool
call arguments, even when the Responses API returns them as dicts."""
def _make_codex_response(self, item_type, arguments, item_status="completed"):
"""Build a minimal Responses API response with a single tool call."""
item = SimpleNamespace(
type=item_type,
status=item_status,
)
if item_type == "function_call":
item.name = "web_search"
item.arguments = arguments
item.call_id = "call_abc123"
item.id = "fc_abc123"
elif item_type == "custom_tool_call":
item.name = "web_search"
item.input = arguments
item.call_id = "call_abc123"
item.id = "fc_abc123"
return SimpleNamespace(
output=[item],
status="completed",
)
def test_function_call_dict_arguments_produce_valid_json(self, agent):
"""dict arguments from function_call must be serialised with
json.dumps, not str(), so downstream json.loads() succeeds."""
args_dict = {"query": "weather in NYC", "units": "celsius"}
response = self._make_codex_response("function_call", args_dict)
msg, _ = agent._normalize_codex_response(response)
tc = msg.tool_calls[0]
parsed = json.loads(tc.function.arguments)
assert parsed == args_dict
def test_custom_tool_call_dict_arguments_produce_valid_json(self, agent):
"""dict arguments from custom_tool_call must also use json.dumps."""
args_dict = {"path": "/tmp/test.txt", "content": "hello"}
response = self._make_codex_response("custom_tool_call", args_dict)
msg, _ = agent._normalize_codex_response(response)
tc = msg.tool_calls[0]
parsed = json.loads(tc.function.arguments)
assert parsed == args_dict
def test_string_arguments_unchanged(self, agent):
"""String arguments must pass through without modification."""
args_str = '{"query": "test"}'
response = self._make_codex_response("function_call", args_str)
msg, _ = agent._normalize_codex_response(response)
tc = msg.tool_calls[0]
assert tc.function.arguments == args_str