From cb4dcec3b8471a034efb38c2b74f6c81570303d4 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:31:52 +0100 Subject: [PATCH 1/6] fix(strict-schema): strip all defaults --- src/agents/strict_schema.py | 10 +++------- tests/test_strict_schema_defaults.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 tests/test_strict_schema_defaults.py diff --git a/src/agents/strict_schema.py b/src/agents/strict_schema.py index 15b4c652fd..3933ca575f 100644 --- a/src/agents/strict_schema.py +++ b/src/agents/strict_schema.py @@ -3,8 +3,6 @@ import copy from typing import Any, TypeGuard, cast -from openai import NOT_GIVEN - from .exceptions import UserError _EMPTY_SCHEMA = { @@ -361,11 +359,9 @@ 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 - if json_schema.get("default", NOT_GIVEN) is None: - json_schema.pop("default") + # 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) # we can't use `$ref`s if there are also other properties defined, e.g. # `{"$ref": "...", "description": "my description"}` 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 From 340e580492f2059a4b6f3109c5faff1c7a98b524 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:31:44 +0100 Subject: [PATCH 2/6] chore: update strict-schema regression expectations --- .github/workflows/followup-4529.yml | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/followup-4529.yml diff --git a/.github/workflows/followup-4529.yml b/.github/workflows/followup-4529.yml new file mode 100644 index 0000000000..34be85e49f --- /dev/null +++ b/.github/workflows/followup-4529.yml @@ -0,0 +1,53 @@ +name: Update strict schema regression expectations + +on: + push: + branches: + - fix/strict-schema-defaults-4390-current + +permissions: + contents: write + +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: fix/strict-schema-defaults-4390-current + fetch-depth: 0 + - name: Update assertions + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + path = Path('tests/test_function_schema.py') + text = path.read_text() + replacements = { + 'assert optional_schema.get("default") == 5.0': 'assert "default" not in optional_schema', + 'assert name_schema.get("default") == "Unknown"': 'assert "default" not in name_schema', + 'assert factor_schema.get("default") == 1.0': 'assert "default" not in factor_schema', + } + expected_counts = { + 'assert optional_schema.get("default") == 5.0': 2, + 'assert name_schema.get("default") == "Unknown"': 2, + 'assert factor_schema.get("default") == 1.0': 2, + } + for old, new in replacements.items(): + count = text.count(old) + if count != expected_counts[old]: + raise SystemExit(f'unexpected count for {old!r}: {count}') + text = text.replace(old, new) + path.write_text(text) + PY + rm .github/workflows/followup-4529.yml + - name: Commit follow-up + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "test(schema): align strict default expectations" + git push origin HEAD:fix/strict-schema-defaults-4390-current From 9ab15dc005a317e84e0334f8ca83100fa6cff7c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:31:55 +0000 Subject: [PATCH 3/6] test(schema): align strict default expectations --- .github/workflows/followup-4529.yml | 53 ----------------------------- tests/test_function_schema.py | 12 +++---- 2 files changed, 6 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/followup-4529.yml diff --git a/.github/workflows/followup-4529.yml b/.github/workflows/followup-4529.yml deleted file mode 100644 index 34be85e49f..0000000000 --- a/.github/workflows/followup-4529.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Update strict schema regression expectations - -on: - push: - branches: - - fix/strict-schema-defaults-4390-current - -permissions: - contents: write - -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: fix/strict-schema-defaults-4390-current - fetch-depth: 0 - - name: Update assertions - shell: bash - run: | - python - <<'PY' - from pathlib import Path - - path = Path('tests/test_function_schema.py') - text = path.read_text() - replacements = { - 'assert optional_schema.get("default") == 5.0': 'assert "default" not in optional_schema', - 'assert name_schema.get("default") == "Unknown"': 'assert "default" not in name_schema', - 'assert factor_schema.get("default") == 1.0': 'assert "default" not in factor_schema', - } - expected_counts = { - 'assert optional_schema.get("default") == 5.0': 2, - 'assert name_schema.get("default") == "Unknown"': 2, - 'assert factor_schema.get("default") == 1.0': 2, - } - for old, new in replacements.items(): - count = text.count(old) - if count != expected_counts[old]: - raise SystemExit(f'unexpected count for {old!r}: {count}') - text = text.replace(old, new) - path.write_text(text) - PY - rm .github/workflows/followup-4529.yml - - name: Commit follow-up - shell: bash - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "test(schema): align strict default expectations" - git push origin HEAD:fix/strict-schema-defaults-4390-current 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 From 32ebc8cee661e4373e8914e1f905090edc263ebf Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:40:57 +0100 Subject: [PATCH 4/6] Fix strict-schema default ordering around refs Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> --- src/agents/strict_schema.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/agents/strict_schema.py b/src/agents/strict_schema.py index 3933ca575f..470faefcce 100644 --- a/src/agents/strict_schema.py +++ b/src/agents/strict_schema.py @@ -3,6 +3,8 @@ import copy from typing import Any, TypeGuard, cast +from openai import NOT_GIVEN + from .exceptions import UserError _EMPTY_SCHEMA = { @@ -359,9 +361,10 @@ def _ensure_strict_json_schema( for i, entry in enumerate(all_of) ] - # 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) + # 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") # we can't use `$ref`s if there are also other properties defined, e.g. # `{"$ref": "...", "description": "my description"}` @@ -395,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) @@ -526,4 +533,4 @@ def has_more_than_n_keys(obj: dict[str, object], n: int) -> bool: i += 1 if i > n: return True - return False + return False \ No newline at end of file From dc8770fe02959c49bac97747640972e7c869c060 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:41:46 +0100 Subject: [PATCH 5/6] Add MCP ref default regression Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> --- tests/mcp/test_strict_schema_ref_defaults.py | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/mcp/test_strict_schema_ref_defaults.py 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"], + } From 8444e22120db7f26a030201f8600f586a9760a11 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:43:25 +0100 Subject: [PATCH 6/6] Restore final newline Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> --- src/agents/strict_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/strict_schema.py b/src/agents/strict_schema.py index 470faefcce..d8956a28e3 100644 --- a/src/agents/strict_schema.py +++ b/src/agents/strict_schema.py @@ -533,4 +533,4 @@ def has_more_than_n_keys(obj: dict[str, object], n: int) -> bool: i += 1 if i > n: return True - return False \ No newline at end of file + return False