## Summary
- preserve boolean, empty, null, type-array, enum, const, and
scalar-constraint semantics across every Python conversion entry point
- intersect Zod enum and const values with declared types and
constraints, including compound JSON values
- default unversioned exact validation to Draft 7 and apply inclusive
and numeric exclusive bounds independently
- run one byte-identical corpus through Python, Zod, and Effect so
accepted and rejected inputs stay aligned
- keep exact JSON Schema acceptance separate from Pydantic default
materialization
## Review follow-up (second push)
- Python: exact Draft 7 acceptance now wraps all three entry points
(`json_schema_to_pydantic_type`, `json_schema_to_model`,
`pydantic_model_from_param_schema`), so they can no longer disagree
- Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI
3.0 style) no longer crash conversion — exact validation falls back to
Draft 4, and the library input is translated to the numeric spelling
- Python: ECMA-only regex patterns (look-around) no longer crash
pydantic model builds — Rust-incompatible patterns fall back to Python
`re`
- Python: type arrays with sibling constraints no longer raise
`TypeError` on valid input — constraints are scoped per member before
the library sees them
- Python: integral floats satisfy `integer`, `const` intersects `enum`,
annotation-only schemas accept anything, and an optional property with
an empty `enum` tolerates absence
- Zod: typeless scalar constraints apply per instance type, and string
lengths count Unicode code points instead of UTF-16 code units
- Effect: draft-4 boolean exclusive bounds are enforced instead of
silently ignored
- `multipleOf` uses decimal scaling in all three converters (declared
`divergesFromJsonSchema` on the corpus case)
- shared corpus grows by 13 primitive cases; new property-based tests
check acceptance against real Draft 7 oracles (hypothesis + `jsonschema`
in Python, fast-check + Ajv in TypeScript)
## Verification
- Python `make chk` (ruff + mypy)
- Python pytest: 1,572 passed (5 langchain-extra tests need an env this
sandbox lacks; unchanged from base)
- `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check
property test; typecheck + build
- `@composio/json-schema-to-effect-schema`: 133 passed; typecheck
- `@composio/core` corpus ingress tests: 61 passed
- shared Python/TypeScript corpus files are byte-identical
(shasum-verified)
- `git diff --check`
## Contributor context
This replaces four narrow proposals after independent local
reproduction:
- [#4301](https://github.com/ComposioHQ/composio/pull/4301) ·
[Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301)
- [#4302](https://github.com/ComposioHQ/composio/pull/4302) ·
[Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302)
- [#4303](https://github.com/ComposioHQ/composio/pull/4303) ·
[Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303)
- [#4307](https://github.com/ComposioHQ/composio/pull/4307) ·
[Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307)
---------
Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
`json_schema_to_model` and `json_schema_to_pydantic_type` now share one
compiled object policy, so provider-facing Pydantic models stop silently
discarding valid free-form arguments and stop ignoring `patternProperties` and
explicit `additionalProperties` controls. Accepted dynamic keys are written back
into `__pydantic_extra__`, which is what `LangchainProvider.wrap_tool` re-reads
with `getattr`.
Acceptance and rejection are asserted from the shared cross-SDK corpus, so the
Python entry points cannot diverge from the TypeScript converters.
This PR:
- builds on top of https://github.com/ComposioHQ/composio/pull/3754
- normalizes list-valued JSON Schema `type` fields before scalar type
lookups
- applies the handling to direct fields and `anyOf`/`oneOf` options
- adds regressions for nullable, single, mixed, unknown, and
combiner-nested type lists
- credits @anxkhn as a Git co-author
## Context
JSON Schema Draft 2020-12 and OpenAPI 3.1 allow `type` to be an array.
The previous parser passed that list to a dictionary lookup, raising
`TypeError: unhashable type: 'list'`. This replacement covers the direct
case from #3754 and the same shape nested in a combiner.
---------
Co-authored-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
## Summary
Fixes `json_schema_to_model` when a top-level JSON schema omits `title`
or sets it to `null`. This is the remaining direct-helper path;
anonymous nested objects from #2435 are already handled by
`schema_converter.py`.
- falls back to `GeneratedModel` only when the title is `None`,
preserving intentionally empty string titles
- adds regression coverage for missing, `null`, and empty titles, model
construction, and required-field validation
## Validation
- `ruff format --check python/composio/utils/shared.py
python/tests/test_schema_parser.py`
- `ruff check python/composio/utils/shared.py
python/tests/test_schema_parser.py`
- `PYTHONPATH="$PWD/python"
/Users/jkomyno/work/composio/composio2/.venv/bin/pytest -q
python/tests/test_schema_parser.py`
This is a follow-up to #2435; it intentionally does not close that
already-resolved issue.
Co-authored-by: jkomyno <alberto@composio.dev>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
## Summary
`get_signature_format_from_schema_params` in
`python/composio/utils/shared.py` builds the `__signature__` for the
tool wrappers used by the langchain, langgraph, llamaindex, and autogen
providers. Its `oneOf`/`anyOf` handling hardcoded a ladder for exactly
1, 2, or 3 union members and raised `ValueError("Invalid 'oneOf'
schema")` for any union with four or more options. It also indexed
`PYDANTIC_TYPE_TO_PYTHON_TYPE[ptype.get("type")]` directly, so any
combiner option missing a `type` key produced `None` and raised
`KeyError(None)`.
Both shapes are valid JSON Schema and appear in real tool input schemas,
so a tool with a 4-way union, or a combiner option without an explicit
`type`, makes those four providers raise at tool-wrap time. This
replaces the ladder with a single map + `functools.reduce` path that
mirrors the existing `_build_union_from_options` in
`schema_converter.py`, keeping the 1/2/3-member and nullable `[type,
null]` outputs identical to before.
Fixes #
## Changes
- `python/composio/utils/shared.py`: in
`get_signature_format_from_schema_params`, replace the 1/2/3-member
`oneOf`/`anyOf` ladder (which raised `ValueError` for 4+ members) with a
map-each-option-to-a-Python-type + `functools.reduce` Union build that
supports any member count. Unknown/missing option types map to
`typing.Any` instead of raising `KeyError`. Adds `from functools import
reduce` and removes three now-unnecessary `# type: ignore` comments.
- `python/tests/test_schema_parser.py`: add
`TestGetSignatureFormatFromSchemaParams` (8 cases) covering 4- and
5-member unions, an `anyOf` option missing `type`, an all-typeless
`anyOf`, and regressions for the previously supported single/2/3-member
and nullable `[type, null]` shapes.
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change
## How Has This Been Tested?
From `python/`:
```
uv run pytest tests/test_schema_parser.py::TestGetSignatureFormatFromSchemaParams -v # 8 passed
uv run pytest tests/test_schema_parser.py -q # 75 passed
make chk # ruff + mypy -> clean, no new ignores
```
To confirm the tests guard the fix, the new test class was also run
against the unmodified `shared.py`: 4 of the 8 fail there
(`ValueError`/`KeyError` on the 4+/typeless cases), and all 8 pass with
this change. The nullable `anyOf [type, null]` case still resolves to
`Union[str, Any, NoneType]` (its pre-existing behavior); collapsing
`null` into `Optional` lives in `schema_converter` and is intentionally
left out of this minimal fix, with the test documenting current
behavior.
## Screenshots (if applicable)
N/A
## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [ ] I updated documentation as needed
- [x] I added tests or explain why not applicable
- [ ] I added a changeset if this change affects published packages
## Additional context
No linked issue: this is a self-identified, reproduced bug in the older
schema->signature path. The fix deliberately mirrors
`schema_converter._build_union_from_options` so the two schema paths
agree on N-ary union handling. The same bug family exists in
`python/composio/utils/openapi.py`
(`function_signature_from_jsonschema`, used by the google_adk provider)
but is kept out of this PR to stay single-purpose; happy to follow up on
it separately.
This PR:
- supersedes https://github.com/ComposioHQ/composio/pull/3625,
https://github.com/ComposioHQ/composio/pull/3629, and the Python
SDK/provider-side fix proposed by
https://github.com/ComposioHQ/composio/pull/3504
- adds `alias_tool_input_schema` / `restore_tool_arguments` for
provider-visible schemas and backend argument restoration
- keeps `substitute_reserved_python_keywords` /
`reinstate_reserved_python_keywords` as compatibility wrappers
- preserves the existing Python keyword alias style (`from` ->
`from_rs`), aliases invalid Python parameter names like `$top`, avoids
leading-underscore aliases so Pydantic-backed providers can build
models, and caps aliases at 64 characters for Anthropic-style tool
schema validators
- dereferences internal `$ref` / `$defs` before aliasing so referenced
object properties are exposed through the same safe provider-visible
names
- restores Gemini manual `handle_response` arguments with the per-tool
alias map before execution
- wraps Google ADK tools with aliased callable signatures and restores
original backend argument names
- wraps Anthropic and Claude Agent SDK tool schemas with the same shared
alias map and restores original backend argument names before execution
- fixes the package-local Claude Agent SDK provider tests so they run
without `pytest-asyncio` and keep using the provider-visible schema
conversion hook
- adds dependency-light core tests for helper behavior, `$ref` aliasing,
Pydantic model compatibility, Gemini manual response, Google ADK
wrapping, Anthropic wrapping, and Claude Agent SDK wrapping
Local validation:
- `uv run --project python pytest
python/tests/test_tool_schema_aliasing.py
python/tests/test_schema_parser.py python/tests/test_json_schema.py
python/tests/test_imports.py -q` -> 112 passed
- `PYTHONPATH=python uv run --project python --with-editable
./python/providers/claude_agent_sdk pytest
python/providers/claude_agent_sdk/tests/test_provider.py -q
--timeout=20` -> 16 passed
- `uv run --project python --with-editable ./python/providers/langchain
pytest --ignore-glob='python/tests/test_type_inference*.py'
python/tests/ -q` -> 742 passed, 31 skipped
- `uv run --project python --with-editable ./python --with-editable
./python/providers/langchain python <LangChain $top wrap smoke>` ->
wrapped args schema field `param_top`
- `uv run --project python --with-editable ./python --with-editable
./python/providers/langgraph python <LangGraph $top wrap smoke>` ->
wrapped args schema field `param_top`
- `uv run --project python ruff check --config python/config/ruff.toml
python/composio/utils/shared.py
python/tests/test_tool_schema_aliasing.py` -> passed
- `uv run --project python ruff format --check --config
python/config/ruff.toml python/composio/utils/shared.py
python/tests/test_tool_schema_aliasing.py` -> passed
- `git diff --check` -> passed
Note: #3500's hosted MCP emission-layer report is already closed; this
PR covers the Python SDK/provider surface rather than changing hosted
MCP schema emission.
## 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>
Schemas for nested objects may use anyOf/allOf/oneOf/$ref instead of a
top-level 'type' key. Direct dict access prop_info['type'] and
prop_info['title'] would raise KeyError in those cases (e.g. Dropbox
SEARCH_FILE_OR_FOLDER with an 'options' parameter).
Changes:
- prop_info['type'] -> prop_info.get('type')
- prop_info['title'] -> prop_info.get('title', prop_name)
- FALLBACK_VALUES[prop_type] -> FALLBACK_VALUES.get(prop_type)
- When prop_type is None, delegate to json_schema_to_pydantic_type()
which already handles all combiner schemas correctly
Fixes#2459
The `_python_reserved` set was hardcoded to only `{"for", "async"}`,
missing `"from"` and all other Python keywords. Tools from toolkits like
Intercom that have a `from` parameter caused `ValueError: 'from' is not
a valid parameter name` when building function signatures.
Extracted keyword substitution logic into shared utilities
(`substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords`)
using `keyword.iskeyword()` from stdlib for complete coverage, and applied
it to all affected providers: langchain, gemini, langgraph, autogen, llamaindex.
Closes PLEN-1671