fix(tools): resolve legacy definitions \$ref in tool schemas for Anthropic and Fireworks

MCP servers (e.g. DevRev) emit tool input schemas that use the draft-04
`definitions` keyword with `\$ref` pointers. When LiteLLM forwards these
to Anthropic or Fireworks, the request fails:

- Anthropic: "PointerToNowhere" — the `definitions` key is stripped by
  the `_allowed_properties` filter while `\$ref` pointers remain
- Fireworks: "Error resolving schema reference" — provider rejects
  unresolved JSON Schema references

Fix: before filtering/forwarding, detect `definitions` in the input
schema and inline all `\$ref` pointers using the existing `unpack_defs`
utility. `\$defs` refs (natively supported by Anthropic) are left as-is.

OpenAI-compatible providers are unaffected — OpenAI resolves `\$ref`
server-side and already works.

Fixes https://github.com/BerriAI/litellm/issues/26692
This commit is contained in:
Mr-Neutr0n 2026-04-28 17:41:24 +05:30
parent 62920a0cb2
commit f1ab7da89d
4 changed files with 191 additions and 0 deletions

View file

@ -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

View file

@ -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(

View file

@ -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"
)

View file

@ -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", {})