diff --git a/src/graphon/dsl/slim/package_loader.py b/src/graphon/dsl/slim/package_loader.py index fc9359c..8c57052 100644 --- a/src/graphon/dsl/slim/package_loader.py +++ b/src/graphon/dsl/slim/package_loader.py @@ -390,6 +390,7 @@ def _convert_credential_form_schema( variable=str(value["variable"]), label=self._convert_i18n(value.get("label")), type=form_type, + help=self._convert_optional_i18n(value.get("help")), required=bool(value.get("required", True)), default=value.get("default"), options=[ diff --git a/src/graphon/model_runtime/entities/provider_entities.py b/src/graphon/model_runtime/entities/provider_entities.py index e7ffe4f..df3ec5d 100644 --- a/src/graphon/model_runtime/entities/provider_entities.py +++ b/src/graphon/model_runtime/entities/provider_entities.py @@ -52,6 +52,7 @@ class CredentialFormSchema(BaseModel): variable: str label: I18nObject type: FormType + help: I18nObject | None = None required: bool = True default: str | None = None options: list[FormOption] | None = None diff --git a/tests/dsl/test_slim_package_loader.py b/tests/dsl/test_slim_package_loader.py index 89d0b6c..d1668f8 100644 --- a/tests/dsl/test_slim_package_loader.py +++ b/tests/dsl/test_slim_package_loader.py @@ -142,3 +142,113 @@ def _write_multi_provider_plugin(plugin_root: Path) -> None: + "\n", encoding="utf-8", ) + + +def test_slim_package_loader_retains_credential_help(tmp_path: Path) -> None: + """The slim loader must carry a manifest's per-field `help` through. + + `_convert_credential_form_schema` builds `CredentialFormSchema` field by + field, so adding the field to the entity alone is not enough on this path — + this test covers that second drop point. + """ + plugin_root = tmp_path / "plugin" + _write_credential_help_plugin(plugin_root) + + binding = SlimProviderBinding( + plugin_id="author/fake:0.0.1@test", + provider="help-provider", + plugin_root=plugin_root, + ) + loader = SlimPackageLoader( + SlimConfig( + bindings=[binding], + local=SlimLocalSettings(folder=tmp_path / "plugins"), + ), + ) + + loaded = loader.load(binding) + credential_schema = loaded.provider_entity.provider_credential_schema + assert credential_schema is not None + form_schemas = { + schema.variable: schema for schema in credential_schema.credential_form_schemas + } + + assert form_schemas["api_key"].help is not None + assert form_schemas["api_key"].help.en_us == "Find this in the provider console." + assert form_schemas["api_key"].help.zh_hans == "在提供商控制台中查找。" + # A field without `help` still loads, unchanged. + assert form_schemas["api_base"].help is None + + +def _write_credential_help_plugin(plugin_root: Path) -> None: + (plugin_root / "_assets").mkdir(parents=True, exist_ok=True) + (plugin_root / "provider").mkdir(parents=True, exist_ok=True) + (plugin_root / "models" / "llm").mkdir(parents=True, exist_ok=True) + + (plugin_root / "manifest.yaml").write_text( + textwrap.dedent( + """ + plugins: + models: + - provider/help.yaml + """, + ).strip() + + "\n", + encoding="utf-8", + ) + (plugin_root / "_assets" / "icon.svg").write_text( + "\n", + encoding="utf-8", + ) + (plugin_root / "provider" / "help.yaml").write_text( + textwrap.dedent( + """ + provider: help-provider + label: + en_US: Help Provider + icon_small: + en_US: icon.svg + supported_model_types: + - llm + configurate_methods: + - predefined-model + models: + llm: + predefined: + - models/llm/help-chat.yaml + provider_credential_schema: + credential_form_schemas: + - variable: api_key + label: + en_US: API Key + type: secret-input + required: true + help: + en_US: Find this in the provider console. + zh_Hans: 在提供商控制台中查找。 + - variable: api_base + label: + en_US: API Base + type: text-input + required: false + """, + ).strip() + + "\n", + encoding="utf-8", + ) + (plugin_root / "models" / "llm" / "help-chat.yaml").write_text( + textwrap.dedent( + """ + model: help-chat + label: + en_US: Help Chat Model + model_type: llm + fetch_from: predefined-model + model_properties: + mode: chat + context_size: 8192 + """, + ).strip() + + "\n", + encoding="utf-8", + ) diff --git a/tests/model_runtime/test_provider_entities.py b/tests/model_runtime/test_provider_entities.py new file mode 100644 index 0000000..bcb26af --- /dev/null +++ b/tests/model_runtime/test_provider_entities.py @@ -0,0 +1,61 @@ +from graphon.model_runtime.entities.provider_entities import ( + CredentialFormSchema, + FormType, + ProviderCredentialSchema, +) + + +def test_credential_form_schema_retains_help_from_manifest() -> None: + """A manifest's per-field `help` must survive unmarshal. + + `dify-plugin-daemon#774` made the daemon return `help` on model-provider + credential fields. Without an explicit field here, pydantic's default + `extra="ignore"` discards the key and no consumer can ever see it. + """ + schema = CredentialFormSchema.model_validate({ + "variable": "api_key", + "label": {"en_US": "API Key"}, + "type": "secret-input", + "help": { + "en_US": "Get your API key from the provider console.", + "zh_Hans": "从提供商控制台获取密钥。", + }, + "required": True, + }) + + assert schema.help is not None + assert schema.help.en_us == "Get your API key from the provider console." + assert schema.help.zh_hans == "从提供商控制台获取密钥。" + assert schema.model_dump()["help"]["en_US"] == ( + "Get your API key from the provider console." + ) + + +def test_credential_form_schema_help_is_optional() -> None: + """A manifest that parses today must keep parsing unchanged.""" + schema = CredentialFormSchema.model_validate({ + "variable": "api_base", + "label": {"en_US": "API Base"}, + "type": FormType.TEXT_INPUT, + }) + + assert schema.help is None + + +def test_provider_credential_schema_retains_nested_help() -> None: + """`help` survives when nested inside a provider credential schema.""" + credential_schema = ProviderCredentialSchema.model_validate({ + "credential_form_schemas": [ + { + "variable": "api_key", + "label": {"en_US": "API Key"}, + "type": "secret-input", + "help": {"en_US": "Found under Settings → API."}, + }, + ], + }) + + assert credential_schema.credential_form_schemas[0].help is not None + assert credential_schema.credential_form_schemas[0].help.en_us == ( + "Found under Settings → API." + )