Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/graphon/dsl/slim/package_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
1 change: 1 addition & 0 deletions src/graphon/model_runtime/entities/provider_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 deletions tests/dsl/test_slim_package_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"<svg xmlns='http://www.w3.org/2000/svg'></svg>\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",
)
61 changes: 61 additions & 0 deletions tests/model_runtime/test_provider_entities.py
Original file line number Diff line number Diff line change
@@ -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."
)
Loading