Skip to content

Commit da9211a

Browse files
fix(dspy): store tool argument defaults in JSON form (#10574)
* fix(dspy): store tool argument defaults in JSON form Tool put each parameter's default into its JSON schema as the raw Python object. An Enum or date default then broke native function calling with "Object of type Unit is not JSON serializable" when the schema was sent to the provider. Convert defaults with pydantic's JSON conversion, and keep the raw value when it has no JSON form. * fix(dspy): keep tool defaults that have no JSON form A non-UTF-8 bytes default made Tool() raise UnicodeDecodeError, and a Decimal default on a float argument was advertised as a string that the tool then rejected. Keep the raw value for Decimal and for any default that fails to convert. * fix(dspy): store Decimal tool defaults as JSON numbers A Decimal default stayed a Decimal in the schema, so serializing the tool for the request still raised TypeError. A float fits both float and Decimal arguments.
1 parent cabcd11 commit da9211a

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

‎dspy/adapters/types/tool.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import asyncio
22
import inspect
3+
from decimal import Decimal
34
from typing import TYPE_CHECKING, Any, Callable, Literal, Protocol, get_origin, get_type_hints
45

56
import json_repair
67
import pydantic
8+
import pydantic_core
79
from jsonschema import ValidationError, validate
810
from pydantic import BaseModel, TypeAdapter, create_model
911

@@ -111,7 +113,7 @@ def _parse_function(self, func: Callable, arg_desc: dict[str, str] | None = None
111113
else:
112114
args[k] = _resolve_json_schema_reference(TypeAdapter(v).json_schema())
113115
if default_values[k] is not inspect.Parameter.empty:
114-
args[k]["default"] = default_values[k]
116+
args[k]["default"] = _to_json_default(default_values[k])
115117
if arg_desc and k in arg_desc:
116118
args[k]["description"] = arg_desc[k]
117119

@@ -520,6 +522,17 @@ def _normalize_tool_call_dict(data: dict[str, Any]) -> dict[str, Any]:
520522
}
521523

522524

525+
def _to_json_default(value: Any) -> Any:
526+
"""Convert a parameter default to its JSON form, e.g. an Enum member to its value."""
527+
if isinstance(value, Decimal):
528+
# A JSON number fits both float and Decimal arguments; pydantic would give a string.
529+
return float(value)
530+
try:
531+
return pydantic_core.to_jsonable_python(value)
532+
except Exception:
533+
return value
534+
535+
523536
def _resolve_json_schema_reference(schema: dict) -> dict:
524537
"""Recursively resolve json model schema, expanding all references."""
525538

‎tests/adapters/test_tool.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,42 @@ def test_tool_from_function():
146146
assert tool.args["y"]["default"] == "hello"
147147

148148

149+
def test_tool_defaults_are_json_serializable():
150+
import datetime
151+
import enum
152+
import json
153+
from decimal import Decimal
154+
155+
class Unit(enum.Enum):
156+
CELSIUS = "celsius"
157+
FAHRENHEIT = "fahrenheit"
158+
159+
def forecast(
160+
city: str,
161+
unit: Unit = Unit.CELSIUS,
162+
start: datetime.date = datetime.date(2026, 1, 2),
163+
price: Decimal = Decimal("1.5"),
164+
factor: float = Decimal("0.1"),
165+
) -> str:
166+
return city
167+
168+
tool = Tool(forecast)
169+
assert tool.args["unit"]["default"] == "celsius"
170+
assert tool.args["start"]["default"] == "2026-01-02"
171+
assert tool.args["price"]["default"] == 1.5
172+
assert tool.args["factor"]["default"] == 0.1
173+
assert tool(city="x", price=tool.args["price"]["default"], factor=tool.args["factor"]["default"]) == "x"
174+
json.dumps(tool.format_as_litellm_function_call())
175+
176+
177+
def test_tool_defaults_without_json_form_are_kept():
178+
def scale(data: bytes = b"\xff") -> str:
179+
return "ok"
180+
181+
tool = Tool(scale)
182+
assert tool.args["data"]["default"] == b"\xff"
183+
184+
149185
def test_format_as_litellm_function_call_excludes_defaulted_args_from_required():
150186
# `x` has no default and must be required; `y` has a default and must be optional.
151187
tool = Tool(dummy_function)

0 commit comments

Comments
 (0)