SDK Language
Python SDK (composio package)
SDK Version
composio 0.9.1 composio_core 0.7.21 composio-client 1.11.0 composio-openai-agents 0.8.0
Runtime Environment
Python: 3.12.x (e.g. 3.12 from project venv) OS: macOS (Darwin 25.x; arm64 or x86_64) Integration: OpenAI Agents SDK via OpenAIAgentsProvider (composio_openai_agents)
Environment
Local Development
Describe the Bug
In composio/core/models/_files.py, FileHelper._substitute_file_uploads_recursively and _substitute_file_downloads_recursively assume every property in schema["properties"] has a JSON Schema object with a top-level "type" key.
Many real tool definitions instead use:
anyOf / oneOf (no top-level "type")
$ref (no top-level "type" on the property node)
When the runtime passes a dict for such a parameter (or when post-processing responses with nested dicts under the same shape), the code does:
params[_param]["type"] == "object"
which raises KeyError: 'type'.
That exception is often surfaced through composio_openai_agents/provider.py as a JSON error string like "error": "'type'", so agents see a cryptic failure even when the remote action may have succeeded.
Observed schema examples that trigger the issue:
- Output
data field: {'$ref': '#/$defs/SomeResponse', 'title': 'Data', 'description': '...'} (no "type")
- Input params using
anyOf for nullable or union types (no top-level "type")
The fix is to use safe access (e.g. params[_param].get("type") == "object") and/or resolve $ref / branch on anyOf before assuming "type" exists.
Steps to Reproduce
- Install the versions above (composio, composio_core, composio-openai-agents).
- Use OpenAI Agents with Composio tools whose schemas include properties without a top-level "type" (common for $ref or anyOf).
- Execute a tool where:
- Upload path: the model supplies a nested dict for a parameter whose schema has no "type" (hits _substitute_file_uploads_recursively), or
- Download path: the response includes nested dict data under a property whose schema has no "type" (hits _substitute_file_downloads_recursively).
- Observe KeyError: 'type' in the stack (or {"successful": false, "error": "'type'"} from the provider wrapper).
Minimal Reproducible Example
Example A — upload path (substitute_file_uploads → KeyError: 'type')
A property schema uses anyOf only (no top-level "type"). The request passes a dict for that parameter, so the code hits params[_param]["type"] and raises.
"""MRE: KeyError('type') in FileHelper.substitute_file_uploads (real SDK)."""
import os
# Must be set before importing composio if default ~/.composio is not writable
os.environ.setdefault("COMPOSIO_CACHE_DIR", "/tmp/composio_cache_mre")
os.makedirs(os.environ["COMPOSIO_CACHE_DIR"], exist_ok=True)
from types import SimpleNamespace
from composio.core.models._files import FileHelper
# Tool only needs .slug, .toolkit.slug, .input_parameters for this code path
tool = SimpleNamespace(
slug="FAKE_TOOL",
toolkit=SimpleNamespace(slug="fake"),
input_parameters={
"type": "object",
"properties": {
"metadata": {
"anyOf": [
{"type": "object", "properties": {"x": {"type": "string"}}},
{"type": "null"},
],
"title": "Metadata",
},
},
"required": [],
},
)
class DummyHttpClient:
"""Not used on this path (no file_uploadable / no from_path)."""
pass
fh = FileHelper(client=DummyHttpClient()) # type: ignore[arg-type]
request = {"metadata": {"x": "nested dict triggers object branch"}}
# Raises: KeyError: 'type'
fh.substitute_file_uploads(tool, request)
Expected: KeyError: 'type' at _substitute_file_uploads_recursively where it does params[_param]["type"] == "object".
Example B — download path (substitute_file_downloads → KeyError: 'type')
Output schema uses $ref (no top-level "type" on the property). The response has a dict under data, so the same pattern fails on the download path.
"""MRE: KeyError('type') in FileHelper.substitute_file_downloads (real SDK)."""
import os
os.environ.setdefault("COMPOSIO_CACHE_DIR", "/tmp/composio_cache_mre")
os.makedirs(os.environ["COMPOSIO_CACHE_DIR"], exist_ok=True)
from types import SimpleNamespace
from composio.core.models._files import FileHelper
tool = SimpleNamespace(
slug="FAKE_TOOL",
toolkit=SimpleNamespace(slug="fake"),
output_parameters={
"type": "object",
"properties": {
"data": {
"$ref": "#/$defs/SomeResponse",
"title": "Data",
"description": "Data from the action execution",
},
},
},
)
class DummyHttpClient:
pass
fh = FileHelper(client=DummyHttpClient()) # type: ignore[arg-type]
response = {"successful": True, "data": {"any": "nested dict"}}
# Raises: KeyError: 'type'
fh.substitute_file_downloads(tool, response)
Expected: KeyError: 'type' at _substitute_file_downloads_recursively on params[_param]["type"] == "object".
Tool is composio_client.types.tool_list_response.Item, a Pydantic model with many required fields (deprecated, etc.). For an MRE, SimpleNamespace is enough: FileHelper only uses tool.slug, tool.toolkit.slug, and tool.input_parameters / tool.output_parameters on these paths :)
Error Output / Stack Trace
Exception:
KeyError: 'type'
Typical stack (upload path, line numbers from composio_core 0.7.21-style layout):
Traceback (most recent call last):
File ".../composio_openai_agents/provider.py", line ~86, in execute_tool_wrapper
await asyncio.to_thread(execute_tool, slug=..., arguments=...)
File ".../composio/.../tools.py", line ..., in execute
...
File ".../composio/core/models/_files.py", line 235, in _substitute_file_uploads_recursively
if isinstance(request[_param], dict) and params[_param]["type"] == "object":
~~~~~~~~~~~~~^^^^^^^^
KeyError: 'type'
Typical stack (download path):
File ".../composio/core/models/_files.py", line 308, in _substitute_file_downloads_recursively
if isinstance(request[_param], dict) and params[_param]["type"] == "object":
~~~~~~~~~~~~~^^^^^^^^
KeyError: 'type'
What the agent sees when the provider catches the exception:
{"successful": false, "error": "'type'", "data": null}
Reproducibility
Additional Context or Screenshots
Suggested fix (for maintainers)
Replace direct params[_param]["type"] with params[_param].get("type") and only recurse into nested object substitution when that value is "object", or extend handling for $ref / anyOf so file substitution logic does not assume a single top-level "type" key.
SDK Language
Python SDK (
composiopackage)SDK Version
composio 0.9.1 composio_core 0.7.21 composio-client 1.11.0 composio-openai-agents 0.8.0
Runtime Environment
Python: 3.12.x (e.g. 3.12 from project venv) OS: macOS (Darwin 25.x; arm64 or x86_64) Integration: OpenAI Agents SDK via OpenAIAgentsProvider (composio_openai_agents)
Environment
Local Development
Describe the Bug
In
composio/core/models/_files.py,FileHelper._substitute_file_uploads_recursivelyand_substitute_file_downloads_recursivelyassume every property inschema["properties"]has a JSON Schema object with a top-level "type" key.Many real tool definitions instead use:
anyOf/oneOf(no top-level"type")$ref(no top-level"type"on the property node)When the runtime passes a dict for such a parameter (or when post-processing responses with nested dicts under the same shape), the code does:
which raises
KeyError: 'type'.That exception is often surfaced through
composio_openai_agents/provider.pyas a JSON error string like"error": "'type'", so agents see a cryptic failure even when the remote action may have succeeded.Observed schema examples that trigger the issue:
datafield:{'$ref': '#/$defs/SomeResponse', 'title': 'Data', 'description': '...'}(no"type")anyOffor nullable or union types (no top-level"type")The fix is to use safe access (e.g.
params[_param].get("type") == "object") and/or resolve$ref/ branch on anyOf before assuming"type"exists.Steps to Reproduce
Minimal Reproducible Example
Example A — upload path (substitute_file_uploads → KeyError: 'type')
A property schema uses anyOf only (no top-level "type"). The request passes a dict for that parameter, so the code hits params[_param]["type"] and raises.
Expected: KeyError: 'type' at _substitute_file_uploads_recursively where it does params[_param]["type"] == "object".
Example B — download path (substitute_file_downloads → KeyError: 'type')
Output schema uses $ref (no top-level "type" on the property). The response has a dict under data, so the same pattern fails on the download path.
Expected: KeyError: 'type' at _substitute_file_downloads_recursively on params[_param]["type"] == "object".
Tool is composio_client.types.tool_list_response.Item, a Pydantic model with many required fields (deprecated, etc.). For an MRE, SimpleNamespace is enough: FileHelper only uses tool.slug, tool.toolkit.slug, and tool.input_parameters / tool.output_parameters on these paths :)
Error Output / Stack Trace
Reproducibility
Additional Context or Screenshots
Suggested fix (for maintainers)
Replace direct params[_param]["type"] with params[_param].get("type") and only recurse into nested object substitution when that value is "object", or extend handling for $ref / anyOf so file substitution logic does not assume a single top-level "type" key.