Repository navigation
Description
What version of camel are you using?
0.2.91a7
System information
- Installed from source (editable), commit
8c791b7b9cf7deab56cb5a92818c34499af9097f(master) - Python 3.12.14
- macOS (Darwin 25.6.0)
- No network needed for the reproduction: the model backend's
runis replaced by a mock returning a pre-builtChatCompletion.
import sys, camel
print(sys.version, sys.platform)
print(camel.__version__)3.12.14 (main, Aug 25 2026, 13:50:33) [Clang 22.1.3] darwin
0.2.91a7
Problem description
With a non-streaming model backend (the default), a tool call whose arguments string is empty or truncated raises a raw json.JSONDecodeError straight out of ChatAgent.step(). The tool is never executed, the agent loop aborts, and the caller gets a JSON decoder error with no indication that it came from a model tool call.
This is not a malformed-input edge case. OpenAI-compatible servers (vLLM, Ollama, Together, and others) commonly emit arguments: "" for zero-argument tools, and the OpenAI Chat Completions specification types function.arguments as a plain str with no JSON guarantee. camel's own streaming path treats an unparseable arguments as an expected input and falls back to the raw string; the non-streaming path does not.
The same payload is tolerated on the streaming path (ChatAgent.step() with model_config_dict={'stream': True} completes normally), so sync-non-streaming and sync-streaming disagree on identical input.
Reproducible example code
The Python snippet (self-contained, no network — model.run is monkeypatched to return a ChatCompletion carrying one tool call with the given arguments string):
from unittest.mock import MagicMock
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice as FullChoice
from openai.types.chat.chat_completion_message_tool_call import (
ChatCompletionMessageToolCall,
Function,
)
from openai.types.completion_usage import CompletionUsage
from camel.agents import ChatAgent
from camel.models import StubModel
from camel.types import ModelType
def ping() -> str:
"""Zero-argument tool."""
return "pong"
def completion(arguments: str) -> ChatCompletion:
return ChatCompletion(
id="mock_response_id",
choices=[
FullChoice(
finish_reason="tool_calls",
index=0,
logprobs=None,
message=ChatCompletionMessage(
role="assistant",
content=None,
tool_calls=[
ChatCompletionMessageToolCall(
id="call_1",
type="function",
function=Function(name="ping", arguments=arguments),
)
],
),
)
],
created=123456789,
model="stub",
object="chat.completion",
usage=CompletionUsage(
completion_tokens=5, prompt_tokens=10, total_tokens=15
),
)
for args_text in ("{}", "", '{"city": "San Fra'):
model = StubModel(ModelType.STUB, model_config_dict={"stream": False})
agent = ChatAgent(
system_message="You are a helpful assistant.",
model=model,
tools=[ping],
max_iteration=1,
)
model.run = MagicMock(return_value=completion(args_text))
agent.model_backend.run = model.run
try:
resp = agent.step("ping please")
print(f"arguments={args_text!r}: OK -> {resp.info['tool_calls']}")
except Exception as e: # noqa: BLE001
print(f"arguments={args_text!r}: RAISED {type(e).__name__}: {e}")Extra dependencies:
openai (already a camel dependency)
Steps to reproduce:
- Save the snippet above and run it with the interpreter that has camel installed.
- Observe that
arguments='{}'executes the tool and returns aToolCallingRecord. - Observe that
arguments=''and the truncatedarguments='{"city": "San Fra'both raisejson.JSONDecodeErrorout ofagent.step(). - Run the same payloads against a
StubModel(..., model_config_dict={"stream": True})agent and observe the streaming path completes without raising.
Actual output (verbatim, as observed in two independent runs):
arguments='{}': OK -> [ToolCallingRecord(tool_name='ping', args={}, result='pong', tool_call_id='call_1', images=None)]
arguments='': RAISED JSONDecodeError: Expecting value: line 1 column 1 (char 0)
arguments='{"city": "San Fra': RAISED JSONDecodeError: Unterminated string starting at: line 1 column 10 (char 9)
The streaming path on the same payloads, for comparison:
== streaming ChatAgent.step() ==
arguments='': no exception; type=StreamingChatAgentResponse; msg=None; info_keys=[]; tool_calls=None
The default configuration (a ChatAgent built with no stream key at all, model_config_dict: {}) behaves like the non-streaming case above:
(default config, no `stream` key) model_config_dict: {} -> RAISED JSONDecodeError Expecting value: line 1 column 1 (char 0)
Traceback
Traceback (most recent call last):
File "<string>", line 11, in <module>
File ".../camel/agents/chat_agent.py", line 2957, in step
return self._step_impl(input_message, response_format)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../camel/agents/chat_agent.py", line 3035, in _step_impl
response = self._get_model_response(
^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../camel/agents/chat_agent.py", line 3714, in _get_model_response
return self._handle_batch_response(response)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../camel/agents/chat_agent.py", line 4039, in _handle_batch_response
args = json.loads(tool_call.function.arguments) # type: ignore[union-attr]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
File ".../python3.12/json/decoder.py", line 338, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File ".../python3.12/json/decoder.py", line 356, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)Expected behavior
A tool call whose arguments string cannot be parsed as JSON should be surfaced as a normal, recoverable condition — the tool is invoked with the raw string (or with {} for the empty string), or the step reports a tool-call error — not abort step() with a bare json.JSONDecodeError. The streaming path already defines exactly this contract.
Concrete basis:
-
Sibling branch that does it right: the streaming accumulator wraps the identical parse in a
json.JSONDecodeErrorhandler and falls back to the raw string —camel/agents/chat_agent.py:5049-5051:try: args = json.loads(tool_call_data['function']['arguments']) except json.JSONDecodeError: args = tool_call_data['function']['arguments']
repeated at
camel/agents/chat_agent.py:6047-6049. The guard is deliberate, and it treats a parse failure as expected input rather than an invariant violation. -
Repo-internal contract elsewhere:
camel/agents/_utils.py:130-134catchesjson.JSONDecodeErrorand returnsNonewith a logged error;camel/agents/chat_agent.py:5006-5011annotates the same failure with the comment "Arguments not complete yet";camel/agents/chat_agent.py:2096catches(json.JSONDecodeError, ValueError, TypeError)for tool-call arguments. -
The OpenAI specification types the field as an unvalidated string. On the NON-streaming
ChatCompletionMessageToolCall.function.argumentsfield (openai/openai-openapiopenapi.yaml, L34499-34506): "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." The field is typedstringwith no JSON guarantee, so an empty or truncatedargumentsis a documented expected input.
Additional context
Root cause (camel/agents/chat_agent.py:4039, in _handle_batch_response):
args = json.loads(tool_call.function.arguments) # type: ignore[union-attr]This is the only unguarded json.loads over a model-supplied arguments string in the non-streaming batch path. _handle_batch_response is called from _get_model_response (chat_agent.py:3714), which is called from _step_impl (chat_agent.py:3035), so the JSONDecodeError escapes ChatAgent.step() before any tool is executed and before any ToolCallingRecord is recorded. Every sibling site that parses the same field — the two streaming accumulators at 5049-5051 and 6047-6049, and _utils.extract_tool_call at 115-133 — already catches json.JSONDecodeError.
Reachability: any ChatAgent(tools=[...], model=<non-streaming OpenAI-compatible backend>) (the default configuration) whose endpoint returns arguments: "" for a zero-argument tool, or a truncated argument string, hits line 4039 on the first tool call.
Fix sketch (not a full patch; happy to open a PR if that's useful): mirror the streaming guard at line 4039,
try:
args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
args = tool_call.function.arguments # or {} for the empty stringwhich brings the non-streaming path in line with chat_agent.py:5049-5053 and 6047-6050.
Related issue/PR references: searched the tracker for JSONDecodeError, arguments, empty arguments, tool_call arguments parse, vLLM tool on both issues and PRs — no existing report of this bug. The closest hits are #2098 ("[BUG] vLLM tool schema doesn't accept strict") and #1827 ("[BUG] Verify and fix Vllm tool call"), both closed and unrelated to argument parsing; #1976 is a closed feature request about FunctionCallingMessage.to_openai_assistant_message. There is an unrelated open issue in the same file, #4336, about the streaming tool_execution_timeout path (_execute_tools_sync_with_status_accumulator, line 5066) — it does not touch _handle_batch_response and does not overlap with this fix.
Happy to open a PR with the approach above once this is confirmed — or happy to be assigned.
AI assistance disclosure: this report was drafted with AI assistance and reviewed/edited by a human; the reproduction was executed and the output above is real, unedited output from the runs shown.
Activity
BlueX888 commentedon Sep 16, 2026
Reproduced this. Root cause: camel/agents/chat_agent.py:4039 -- _handle_batch_response parses tool-call arguments with a bare json.loads(tool_call.function.arguments), while the streaming path at camel/agents/chat_agent.py:5050 wraps the identical call in try/except json.JSONDecodeError and falls back to the raw string.
Minimal reproduction:
Observed output:
Happy to open a PR with this fix, or to be assigned if you prefer to take it.
fengju0213 commentedon Sep 18, 2026
Thanks for the report and PR. Surfacing errors from invalid tool-call arguments is expected behavior. We will close issue #4338 and PR #4339.