mirror of
https://github.com/ComposioHQ/composio.git
synced 2026-09-22 11:46:35 +08:00
ce4b213361
## Summary Models — and some MCP transports — occasionally emit tool-call arguments as a **JSON string instead of an object/dict**. The most visible trigger is `COMPOSIO_MULTI_EXECUTE_TOOL` on the Vercel AI SDK, where streaming fails with: > `messages.3.content.1.tool_use.input: Input should be a valid dictionary` Until now only a handful of providers guarded against this, each with its own slightly different inline check, leaving most providers vulnerable and behaviour inconsistent across the SDK. This PR centralizes the coercion into **one helper per language** and routes **every** provider through it, in both the TypeScript and Python SDKs. Closes https://github.com/ComposioHQ/composio/issues/2406 ## What changed **TypeScript** — new `normalizeToolArguments` in `@composio/core` (exported), used by every provider: `vercel`, `cloudflare`, `openai-agents`, `openai` (ChatCompletions + Responses), `anthropic`, `google`, `langchain`, `llamaindex`, `claude-agent-sdk`, `mastra`. **Python** — new `normalize_tool_arguments` in `composio.utils.shared`, used by every provider: `openai`, `openai-responses`, `anthropic`, `google`, `langchain`, `langgraph`, `crewai`, `autogen`, `llamaindex`, `gemini`, `google-adk`, `openai-agents`, `claude-agent-sdk`. Shared semantics (identical in both languages): | Input | Result | | --- | --- | | object / dict | returned unchanged | | JSON string | parsed to object | | empty / whitespace string | `{}` | | `null` / `undefined` / `None` | `{}` | | array, primitive, unparseable string, JSON that isn't an object | **typed error** (`ComposioInvalidToolArgumentsError` / `InvalidParams`) with the original parse error as cause | The typed error replaces the previous grab-bag of behaviours: a raw `SyntaxError`/`JSONDecodeError`, or — worse — silently forwarding a malformed string downstream. ## Why this supersedes the open PRs This consolidates and extends three open PRs that each addressed a slice of the problem inconsistently. Their authors are credited as co-authors on the relevant commits: - **#3489** (LlamaIndex + Claude Agent SDK, TS) — @srijanarya - **#3438** (Anthropic, Google, LangChain, TS) — @aptsalt - **#3437** (Google ADK empty schemas + name fix, Python) — @pragnyanramtha — its empty-`input_parameters` / missing-description handling and the `gemini` → `google_adk` provider-name fix are folded in here. Compared to the three combined, this PR additionally: covers **every** provider in **both** SDKs (not a subset of one), defines a single source of truth instead of per-provider snippets, normalizes empty/`null` payloads to `{}`, and raises an actionable typed error instead of leaking `SyntaxError` or forwarding a bad string. ## Tests - Exhaustive unit tests for both helpers (object passthrough, JSON-string parse, empty/null → `{}`, malformed/non-object → typed error). - Per-provider regression tests across the touched TypeScript providers (string path, malformed-string path, empty-payload path). - Full `@composio/core` + touched-provider TS suites pass; `typecheck` and `lint` clean. Python `ruff` clean and new test green. ## Changeset Patch bump for all affected TypeScript packages (`@composio/core` + the providers). Python follows its own versioning, so no changeset there. --------- Co-authored-by: srijanarya <74669415+srijanarya@users.noreply.github.com> Co-authored-by: Deepak Singh Kandari <deepaksinghkandari07@gmail.com> Co-authored-by: Pragnyan Ramtha <pragnyanramtha@gmail.com>
45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
"""Tests for normalize_tool_arguments (issue #2406)."""
|
|
|
|
import pytest
|
|
|
|
from composio.exceptions import InvalidParams
|
|
from composio.utils.shared import normalize_tool_arguments
|
|
|
|
pytestmark = pytest.mark.core
|
|
|
|
|
|
class TestNormalizeToolArguments:
|
|
def test_dict_is_returned_unchanged(self):
|
|
payload = {"to": "a@b.com", "subject": "hi"}
|
|
assert normalize_tool_arguments(payload) is payload
|
|
|
|
def test_json_string_is_parsed(self):
|
|
payload = {"to": "a@b.com", "subject": "hi", "body": "Hello"}
|
|
assert (
|
|
normalize_tool_arguments(
|
|
'{"to": "a@b.com", "subject": "hi", "body": "Hello"}'
|
|
)
|
|
== payload
|
|
)
|
|
|
|
def test_none_becomes_empty_dict(self):
|
|
assert normalize_tool_arguments(None) == {}
|
|
|
|
@pytest.mark.parametrize("value", ["", " ", "\n\t "])
|
|
def test_empty_string_becomes_empty_dict(self, value):
|
|
assert normalize_tool_arguments(value) == {}
|
|
|
|
def test_malformed_json_string_raises(self):
|
|
with pytest.raises(InvalidParams, match="not valid JSON"):
|
|
normalize_tool_arguments('{"to": "a@b.com"')
|
|
|
|
@pytest.mark.parametrize("value", ["[1, 2, 3]", "42", '"hello"'])
|
|
def test_non_object_json_raises(self, value):
|
|
with pytest.raises(InvalidParams, match="must resolve to an object"):
|
|
normalize_tool_arguments(value)
|
|
|
|
@pytest.mark.parametrize("value", [[1, 2, 3], 42, True])
|
|
def test_non_dict_value_raises(self, value):
|
|
with pytest.raises(InvalidParams, match="must resolve to an object"):
|
|
normalize_tool_arguments(value)
|