diff --git a/api/core/workflow/nodes/agent/runtime_support.py b/api/core/workflow/nodes/agent/runtime_support.py index a872774c98c..9a36e87e015 100644 --- a/api/core/workflow/nodes/agent/runtime_support.py +++ b/api/core/workflow/nodes/agent/runtime_support.py @@ -198,8 +198,23 @@ class AgentRuntimeSupport: if model_schema: model_schema = self._remove_unsupported_model_features_for_old_version(model_schema) value["entity"] = model_schema.model_dump(mode="json") + # The model selector value from the workflow frontend only + # carries provider/model/mode — it does NOT include + # completion_params. AgentStrategy plugins (cot_agent, + # function_calling) read completion_params to build the + # LLMModelConfig that is backwards-invoked, and some model + # providers raise KeyError('required') when + # completion_params is empty because their parameter_rules + # declare required fields with no default. Populate + # completion_params with the defaults declared in the model + # schema so the plugin daemon always receives a valid set + # of model parameters. + if "completion_params" not in value: + value["completion_params"] = self._extract_default_completion_params(model_schema) else: value["entity"] = None + if "completion_params" not in value: + value["completion_params"] = {} result[parameter_name] = value return result @@ -275,6 +290,24 @@ class AgentRuntimeSupport: model_schema.features.remove(feature) return model_schema + @staticmethod + def _extract_default_completion_params(model_schema: AIModelEntity) -> dict[str, Any]: + """Build a completion_params dict from the model schema's parameter_rules. + + The workflow Agent node's model-selector parameter only stores + provider/model/mode — it never carries completion_params. When the + value is forwarded to the plugin daemon, AgentModelConfig defaults + completion_params to ``{}``, which causes some model providers to fail + because their parameter_rules declare required fields. This helper + collects the ``default`` value of every parameter_rule that has one so + the plugin daemon receives a valid, non-empty set of model parameters. + """ + completion_params: dict[str, Any] = {} + for rule in model_schema.parameter_rules: + if rule.default is not None: + completion_params[rule.name] = rule.default + return completion_params + @staticmethod def _filter_mcp_type_tool( strategy: ResolvedAgentStrategy, diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py b/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py index c86de7f6e63..f79f4282649 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py @@ -2,7 +2,16 @@ from types import SimpleNamespace from unittest.mock import Mock, patch from core.workflow.nodes.agent.runtime_support import AgentRuntimeSupport -from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ( + AIModelEntity, + FetchFrom, + ModelFeature, + ModelPropertyKey, + ModelType, + ParameterRule, + ParameterType, +) def test_fetch_model_reuses_single_model_assembly(): @@ -47,3 +56,98 @@ def test_fetch_model_reuses_single_model_assembly(): model_type=ModelType.LLM, model="gpt-4o-mini", ) + + +def _make_model_schema_with_defaults() -> AIModelEntity: + """Return a minimal AIModelEntity whose parameter_rules carry defaults.""" + return AIModelEntity( + model="qwen-max", + label=I18nObject(en_US="Qwen Max"), + model_type=ModelType.LLM, + features=[ModelFeature.AGENT_THOUGHT, ModelFeature.MULTI_TOOL_CALL], + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={ + ModelPropertyKey.MODE: "chat", + ModelPropertyKey.CONTEXT_SIZE: 32768, + }, + parameter_rules=[ + ParameterRule( + name="temperature", + use_template="temperature", + label=I18nObject(en_US="Temperature"), + type=ParameterType.FLOAT, + required=False, + default=0.7, + min=0.0, + max=2.0, + precision=2, + ), + ParameterRule( + name="max_tokens", + use_template="max_tokens", + label=I18nObject(en_US="Max Tokens"), + type=ParameterType.INT, + required=False, + default=2048, + min=1, + max=32768, + ), + ParameterRule( + name="top_p", + use_template="top_p", + label=I18nObject(en_US="Top P"), + type=ParameterType.FLOAT, + required=False, + default=1.0, + ), + ], + ) + + +def test_extract_default_completion_params_collects_rule_defaults(): + """_extract_default_completion_params should gather every rule.default.""" + schema = _make_model_schema_with_defaults() + params = AgentRuntimeSupport._extract_default_completion_params(schema) + assert params == {"temperature": 0.7, "max_tokens": 2048, "top_p": 1.0} + + +def test_extract_default_completion_params_skips_rules_without_default(): + """Rules whose default is None must not appear in the result.""" + schema = AIModelEntity( + model="test-model", + label=I18nObject(en_US="Test"), + model_type=ModelType.LLM, + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={ModelPropertyKey.MODE: "chat"}, + parameter_rules=[ + ParameterRule( + name="seed", + label=I18nObject(en_US="Seed"), + type=ParameterType.INT, + required=False, + default=None, + ), + ParameterRule( + name="temperature", + label=I18nObject(en_US="Temperature"), + type=ParameterType.FLOAT, + required=False, + default=0.5, + ), + ], + ) + params = AgentRuntimeSupport._extract_default_completion_params(schema) + assert params == {"temperature": 0.5} + + +def test_extract_default_completion_params_empty_when_no_defaults(): + """An empty parameter_rules list yields an empty dict.""" + schema = AIModelEntity( + model="test-model", + label=I18nObject(en_US="Test"), + model_type=ModelType.LLM, + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={ModelPropertyKey.MODE: "chat"}, + parameter_rules=[], + ) + assert AgentRuntimeSupport._extract_default_completion_params(schema) == {}