diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 31b9bad3395..06741d1df49 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -448,6 +448,28 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if "properties" not in _input_schema: _input_schema["properties"] = {} + # Resolve legacy `definitions` $ref pointers inline before + # filtering. Anthropic supports `$defs` natively but not + # `definitions` (old JSON Schema draft-04 style). MCP servers + # such as DevRev emit schemas with `definitions` + `$ref` which + # causes Anthropic to return "PointerToNowhere" errors because + # the `definitions` key is stripped by the _allowed_properties + # filter below while the $ref pointers remain. + if "definitions" in _input_schema: + import copy + + from litellm.litellm_core_utils.prompt_templates.common_utils import ( + unpack_defs, + ) + + _input_schema = copy.deepcopy(_input_schema) + defs: dict = { + **_input_schema.pop("definitions", {}), + **_input_schema.pop("$defs", {}), + } + if defs: + unpack_defs(_input_schema, defs) + _allowed_properties = set(AnthropicInputSchema.__annotations__.keys()) input_schema_filtered = { k: v for k, v in _input_schema.items() if k in _allowed_properties diff --git a/litellm/llms/fireworks_ai/chat/transformation.py b/litellm/llms/fireworks_ai/chat/transformation.py index ed6d167a118..d84c18fa09f 100644 --- a/litellm/llms/fireworks_ai/chat/transformation.py +++ b/litellm/llms/fireworks_ai/chat/transformation.py @@ -200,9 +200,27 @@ class FireworksAIConfig(OpenAIGPTConfig): def _transform_tools( self, tools: List[OpenAIChatCompletionToolParam] ) -> List[OpenAIChatCompletionToolParam]: + import copy + for tool in tools: if tool.get("type") == "function": tool["function"].pop("strict", None) + # Resolve `definitions` $ref pointers inline — Fireworks + # rejects tool schemas with unresolved JSON Schema references. + params = tool["function"].get("parameters") + if params and "definitions" in params: + from litellm.litellm_core_utils.prompt_templates.common_utils import ( + unpack_defs, + ) + + params = copy.deepcopy(params) + defs: dict = { + **params.pop("definitions", {}), + **params.pop("$defs", {}), + } + if defs: + unpack_defs(params, defs) + tool["function"]["parameters"] = params return tools def _transform_messages_helper( diff --git a/tests/llm_translation/test_anthropic_completion.py b/tests/llm_translation/test_anthropic_completion.py index fdf8c24ac9e..6b6bddd71aa 100644 --- a/tests/llm_translation/test_anthropic_completion.py +++ b/tests/llm_translation/test_anthropic_completion.py @@ -1885,3 +1885,110 @@ def test_metadata_filter_applies_to_azure_anthropic(): headers={}, ) assert data.get("metadata") == {"user_id": "u2"} + + +def test_anthropic_tool_with_legacy_definitions_ref_resolved_inline(): + """MCP servers (e.g. DevRev) emit schemas with `definitions` + $ref. + Anthropic doesn't support `definitions` — refs must be inlined before + the _allowed_properties filter strips the definitions block.""" + args = { + "non_default_params": { + "tools": [ + { + "type": "function", + "function": { + "name": "update_contact", + "description": "Update a contact.", + "parameters": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "tags": { + "$ref": "#/definitions/_gen:tags", + "description": "Tags for the contact.", + }, + }, + "required": ["id"], + "definitions": { + "_gen:tags": { + "type": "object", + "properties": { + "set": { + "type": "array", + "items": {"type": "string"}, + } + }, + } + }, + }, + }, + } + ] + } + } + + mapped_params = litellm.AnthropicConfig().map_openai_params( + non_default_params=args["non_default_params"], + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + tool = mapped_params["tools"][0] + input_schema = tool["input_schema"] + + # `definitions` must not appear — it's not in AnthropicInputSchema + assert "definitions" not in input_schema + + # The $ref should have been resolved inline — no dangling pointer + tags_prop = input_schema["properties"]["tags"] + assert "$ref" not in tags_prop, "dangling $ref will cause Anthropic PointerToNowhere" + assert tags_prop.get("type") == "object" + assert "set" in tags_prop.get("properties", {}) + + +def test_anthropic_tool_defs_ref_still_preserved(): + """$defs refs (officially supported by Anthropic) must not be touched.""" + args = { + "non_default_params": { + "tools": [ + { + "type": "function", + "function": { + "name": "create_user", + "description": "Create a user.", + "parameters": { + "type": "object", + "properties": { + "user": {"$ref": "#/$defs/User"}, + }, + "required": ["user"], + "$defs": { + "User": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + }, + } + }, + }, + }, + } + ] + } + } + + mapped_params = litellm.AnthropicConfig().map_openai_params( + non_default_params=args["non_default_params"], + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + tool = mapped_params["tools"][0] + # $defs refs should be preserved as-is (Anthropic handles them natively) + assert tool["input_schema"]["properties"]["user"]["$ref"] == "#/$defs/User" + assert ( + tool["input_schema"]["$defs"]["User"]["properties"]["name"]["type"] == "string" + ) diff --git a/tests/llm_translation/test_fireworks_ai_translation.py b/tests/llm_translation/test_fireworks_ai_translation.py index 1cc6aabdca8..cb83a942f60 100644 --- a/tests/llm_translation/test_fireworks_ai_translation.py +++ b/tests/llm_translation/test_fireworks_ai_translation.py @@ -242,3 +242,47 @@ def test_global_disable_flag_with_transform_messages_helper(monkeypatch): "#transform=inline" not in json_data["messages"][0]["content"][1]["image_url"]["url"] ) + + +def test_fireworks_transform_tools_resolves_definitions_refs(): + """MCP servers emit tool schemas with `definitions` + $ref. Fireworks + rejects these — verify _transform_tools resolves refs inline.""" + from litellm.llms.fireworks_ai.chat.transformation import FireworksAIConfig + + tools = [ + { + "type": "function", + "function": { + "name": "update_contact", + "description": "Update a contact.", + "parameters": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "tags": { + "$ref": "#/definitions/_gen:tags", + "description": "Tags", + }, + }, + "required": ["id"], + "definitions": { + "_gen:tags": { + "type": "object", + "properties": { + "set": {"type": "array", "items": {"type": "string"}} + }, + } + }, + }, + }, + } + ] + + result = FireworksAIConfig()._transform_tools(tools) + params = result[0]["function"]["parameters"] + + assert "definitions" not in params, "definitions block must be stripped" + tags_prop = params["properties"]["tags"] + assert "$ref" not in tags_prop, "dangling $ref must be resolved" + assert tags_prop.get("type") == "object" + assert "set" in tags_prop.get("properties", {})