diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index d483fc9cc..d5ab3a95e 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -982,11 +982,18 @@ def add_definition( if not name.strip(): raise ValueError("tool definition names must not be empty") + extras = dict(extra_fields or {}) + overlap = {"kind", "ref", "settings"}.intersection(extras) + if overlap: + raise ValueError( + "extra_fields duplicates known fields: " + + ", ".join(sorted(overlap)) + ) value = { "kind": kind, "ref": ref, "settings": dict(settings or {}), - **dict(extra_fields or {}), + **extras, } self.definitions[name] = ToolDefinitionConfig.model_validate(value) return self diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index 6fe5b8b73..5c7c878cc 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -763,6 +763,19 @@ def test_typed_tool_definition_omits_empty_settings(): } +@pytest.mark.parametrize("field", ["kind", "ref", "settings"]) +def test_typed_tool_definition_rejects_known_extra_field_collisions(field: str): + tools = ToolsConfig() + + with pytest.raises(ValueError, match="extra_fields duplicates known fields"): + tools.add_definition( + "web", + kind="function_group", + ref="web_tools", + extra_fields={field: "replacement"}, + ) + + def test_run_plan_snapshot_removes_named_definition(): config = _FabricConfigSnapshot.from_mapping( {