diff --git a/src/agents/strict_schema.py b/src/agents/strict_schema.py index 15b4c652fd..d8956a28e3 100644 --- a/src/agents/strict_schema.py +++ b/src/agents/strict_schema.py @@ -361,9 +361,8 @@ def _ensure_strict_json_schema( for i, entry in enumerate(all_of) ] - # strip `None` defaults as there's no meaningful distinction here - # the schema will still be `nullable` and the model will default - # to using `None` anyway + # Preserve the historical early handling for `default: None`: it is non-constraining and + # should not force a `$ref` expansion by itself. if json_schema.get("default", NOT_GIVEN) is None: json_schema.pop("default") @@ -399,6 +398,10 @@ def _ensure_strict_json_schema( inside_nested_resource=inside_nested_resource, ) + # Strict schemas mark every property as required, so API-level defaults are not used and + # must be omitted because Structured Outputs rejects `default` in property definitions. + json_schema.pop("default", None) + if budget.reject_open_objects and "$ref" in json_schema: raise UserError(_UNVALIDATED_REF_ERROR) diff --git a/tests/mcp/test_strict_schema_ref_defaults.py b/tests/mcp/test_strict_schema_ref_defaults.py new file mode 100644 index 0000000000..516faccff9 --- /dev/null +++ b/tests/mcp/test_strict_schema_ref_defaults.py @@ -0,0 +1,37 @@ +from agents.mcp import MCPUtil + +from .helpers import FakeMCPServer + + +def test_mcp_ref_with_non_null_default_remains_strict() -> None: + server = FakeMCPServer() + server.add_tool( + "currency_tool", + { + "type": "object", + "properties": { + "currency": { + "$ref": "#/$defs/Currency", + "default": "EUR", + } + }, + "$defs": { + "Currency": { + "type": "string", + "enum": ["EUR", "USD"], + } + }, + }, + ) + + function_tool = MCPUtil.to_function_tool(server.tools[0], server, True) + + assert function_tool.strict_json_schema is True + assert function_tool.params_json_schema["required"] == ["currency"] + assert function_tool.params_json_schema["additionalProperties"] is False + + currency_schema = function_tool.params_json_schema["properties"]["currency"] + assert currency_schema == { + "type": "string", + "enum": ["EUR", "USD"], + } diff --git a/tests/test_function_schema.py b/tests/test_function_schema.py index 1b261ce9b5..7abc5104d1 100644 --- a/tests/test_function_schema.py +++ b/tests/test_function_schema.py @@ -501,7 +501,7 @@ def func_with_optional_field( optional_schema = properties.get("optional_param", {}) assert optional_schema.get("type") == "number" assert optional_schema.get("minimum") == 0.0 # ge=0.0 - assert optional_schema.get("default") == 5.0 + assert "default" not in optional_schema # Valid input with default valid_input = {"required_param": "test"} @@ -675,13 +675,13 @@ def func_with_multiple_field_constraints( assert name_schema.get("type") == "string" assert name_schema.get("minLength") == 1 assert name_schema.get("maxLength") == 50 - assert name_schema.get("default") == "Unknown" + assert "default" not in name_schema # Check factor field factor_schema = properties.get("factor", {}) assert factor_schema.get("type") == "number" assert factor_schema.get("exclusiveMinimum") == 0.0 - assert factor_schema.get("default") == 1.0 + assert "default" not in factor_schema assert factor_schema.get("description") == "Positive multiplier" # Valid input with defaults @@ -761,7 +761,7 @@ def func_with_annotated_optional_field( optional_schema = properties.get("optional_param", {}) assert optional_schema.get("type") == "number" assert optional_schema.get("minimum") == 0.0 # ge=0.0 - assert optional_schema.get("default") == 5.0 + assert "default" not in optional_schema # Valid input with default valid_input = {"required_param": "test"} @@ -854,13 +854,13 @@ def func_with_annotated_multiple_field_constraints( assert name_schema.get("type") == "string" assert name_schema.get("minLength") == 1 assert name_schema.get("maxLength") == 50 - assert name_schema.get("default") == "Unknown" + assert "default" not in name_schema # Check factor field factor_schema = properties.get("factor", {}) assert factor_schema.get("type") == "number" assert factor_schema.get("exclusiveMinimum") == 0.0 - assert factor_schema.get("default") == 1.0 + assert "default" not in factor_schema assert factor_schema.get("description") == "Positive multiplier" # Valid input with defaults diff --git a/tests/test_strict_schema_defaults.py b/tests/test_strict_schema_defaults.py new file mode 100644 index 0000000000..6ecfa56d8a --- /dev/null +++ b/tests/test_strict_schema_defaults.py @@ -0,0 +1,19 @@ +from agents.strict_schema import ensure_strict_json_schema + + +def test_strict_schema_removes_all_property_defaults() -> None: + schema = { + "type": "object", + "properties": { + "currency": {"type": "string", "default": "EUR"}, + "retries": {"type": "integer", "default": 3}, + "enabled": {"type": "boolean", "default": False}, + "note": {"type": ["string", "null"], "default": None}, + }, + } + + result = ensure_strict_json_schema(schema) + + assert result["required"] == ["currency", "retries", "enabled", "note"] + for property_schema in result["properties"].values(): + assert "default" not in property_schema