fix: expose dynamic custom providers in WebUI settings

maintainer edit: WebUI settings still treated non-registry custom providers as unknown, so users could not select them in model configurations or fetch their model list. Reuse dynamic provider specs for settings payloads, model-list requests, and provider updates.
This commit is contained in:
chengyongru
2026-06-13 00:04:13 +08:00
committed by Xubin Ren
parent 68c6844c0b
commit 37ae655fa6
2 changed files with 220 additions and 47 deletions
+136
View File
@@ -18,6 +18,7 @@ from nanobot.webui.settings_api import (
update_agent_settings,
update_model_configuration,
update_network_safety_settings,
update_provider_settings,
update_transcription_settings,
)
@@ -64,6 +65,38 @@ def test_create_model_configuration_writes_label_and_selects(
assert duplicate.value.status == 409
def test_create_model_configuration_accepts_dynamic_custom_provider(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
config = Config.model_validate(
{
"providers": {
"my-company-api": {
"apiBase": "https://example.test/v1",
}
}
}
)
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
payload = create_model_configuration(
{
"label": ["Tenant model"],
"provider": ["my-company-api"],
"model": ["gpt-4o-mini"],
}
)
assert payload["agent"]["model_preset"] == "tenant-model"
assert payload["agent"]["provider"] == "my-company-api"
saved = load_config(config_path)
assert saved.model_presets["tenant-model"].provider == "my-company-api"
assert saved.model_presets["tenant-model"].model == "gpt-4o-mini"
def test_create_model_configuration_rejects_unconfigured_provider(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
@@ -124,6 +157,40 @@ def test_update_model_configuration_edits_named_preset_and_selects(
assert saved.model_presets["codex"].model == "openai-codex/gpt-5.5"
def test_update_provider_settings_updates_dynamic_custom_provider(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
config = Config.model_validate(
{
"providers": {
"my-company-api": {
"apiBase": "https://old.example/v1",
}
}
}
)
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
payload = update_provider_settings(
{
"provider": ["my-company-api"],
"apiBase": ["https://new.example/v1"],
"apiKey": ["sk-test"],
}
)
providers = {row["name"]: row for row in payload["providers"]}
assert providers["my-company-api"]["api_base"] == "https://new.example/v1"
assert providers["my-company-api"]["api_key_hint"] == "••••"
saved = load_config(config_path)
dynamic_provider = saved.providers.model_extra["my-company-api"]
assert dynamic_provider.api_base == "https://new.example/v1"
assert dynamic_provider.api_key == "sk-test"
def test_update_agent_settings_accepts_context_window_options(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
@@ -223,6 +290,39 @@ def test_settings_payload_includes_oauth_provider_status(
assert providers["openai_codex"]["oauth_account"] == "acct-test"
def test_settings_payload_includes_dynamic_custom_provider(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
config = Config.model_validate(
{
"agents": {
"defaults": {
"provider": "my-company-api",
"model": "gpt-4o-mini",
}
},
"providers": {
"my-company-api": {
"apiBase": "https://example.test/v1",
}
},
}
)
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
payload = settings_payload()
providers = {row["name"]: row for row in payload["providers"]}
assert payload["agent"]["provider"] == "my-company-api"
assert payload["agent"]["resolved_provider"] == "my-company-api"
assert providers["my-company-api"]["configured"] is True
assert providers["my-company-api"]["api_key_required"] is False
assert providers["my-company-api"]["api_base"] == "https://example.test/v1"
def test_settings_payload_includes_network_safety_fields(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
@@ -678,6 +778,42 @@ def test_provider_models_payload_fetches_openai_compatible_models(
assert payload["models"][1]["context_window"] == 65536
def test_provider_models_payload_fetches_dynamic_custom_provider_models(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
config = Config.model_validate(
{
"providers": {
"my-company-api": {
"apiBase": "https://example.test/v1",
}
}
}
)
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
def fake_get(url: str, **kwargs):
assert url == "https://example.test/v1/models"
assert "Authorization" not in kwargs["headers"]
return httpx.Response(
200,
json={"data": [{"id": "custom-gpt", "owned_by": "example"}]},
request=httpx.Request("GET", url),
)
monkeypatch.setattr("nanobot.webui.settings_api.httpx.get", fake_get)
payload = provider_models_payload({"provider": ["my-company-api"]})
assert payload["provider"] == "my-company-api"
assert payload["status"] == "available"
assert payload["catalog_kind"] == "custom"
assert payload["models"][0]["id"] == "custom-gpt"
@pytest.mark.parametrize(
("api_base", "expected_url"),
[