From da4d3d097ad29722cd4463c58ddecadf971570ec Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 24 Jul 2026 17:11:39 +0800 Subject: [PATCH] core/config: persist provider models as TOML inline tables --- src/plyngent/config/store.py | 52 ++++++++++++++++++++++++++------ tests/test_config/test_config.py | 30 ++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/plyngent/config/store.py b/src/plyngent/config/store.py index c8244f6..4528489 100644 --- a/src/plyngent/config/store.py +++ b/src/plyngent/config/store.py @@ -395,8 +395,48 @@ class ConfigStore: """Sync ``[plugins]`` to the document.""" self._sync_section("plugins", self._plugins) + @staticmethod + def _to_inline_table(mapping: Mapping[str, object]) -> object: + """Encode a flat (or nested-dict) mapping as a TOML inline table.""" + table = tomlkit.inline_table() + for key, value in mapping.items(): + if isinstance(value, dict): + table[key] = ConfigStore._to_inline_table(cast("Mapping[str, object]", value)) + else: + table[key] = value + return table + + @staticmethod + def _models_to_toml_table(models: Mapping[str, object]) -> object: + """Write each model as ``name = { … }`` under ``[providers.*.models]``.""" + table = tomlkit.table() + for model_id, cfg in models.items(): + if isinstance(cfg, dict): + table[model_id] = ConfigStore._to_inline_table(cast("Mapping[str, object]", cfg)) + else: + table[model_id] = ConfigStore._to_inline_table({}) + return table + + @staticmethod + def _provider_to_toml_table(raw: Mapping[str, object]) -> object: + """Build a provider table; nested maps become inline tables except ``models``.""" + entry = tomlkit.table() + for key, value in raw.items(): + if key == "models" and isinstance(value, dict): + entry[key] = ConfigStore._models_to_toml_table(cast("Mapping[str, object]", value)) + elif isinstance(value, dict): + entry[key] = ConfigStore._to_inline_table(cast("Mapping[str, object]", value)) + else: + entry[key] = value + return entry + def _sync_providers_section(self) -> None: - """Sync ``[providers]`` to the document (ready + recoverable).""" + """Sync ``[providers]`` to the document (ready + recoverable). + + Model configs are written as **inline tables** under + ``[providers..models]`` (e.g. ``\"gpt-test\" = { text = true }``), + not as one dotted section per model id. + """ section = self._toml_table("providers") keep = set(self._providers) | set(self._recoverable_providers) @@ -405,14 +445,8 @@ class ConfigStore: del section[name] for name, provider in {**self._recoverable_providers, **self._providers}.items(): - raw: dict[str, object] = msgspec.to_builtins(provider) - if name in section: - entry = cast("MutableMapping[str, object]", section[name]) - entry.clear() - for k, v in raw.items(): - entry[k] = v - else: - section[name] = raw + raw = cast("dict[str, object]", msgspec.to_builtins(provider)) + section[name] = self._provider_to_toml_table(raw) def _sync_to_document(self) -> None: """Incrementally sync all sections into the document.""" diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index eba4789..fceb8d1 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -3,6 +3,7 @@ from collections.abc import Mapping from pathlib import Path import pytest +import tomlkit import plyngent from plyngent.config import ( @@ -12,6 +13,7 @@ from plyngent.config import ( OpenAICompatibleProvider, OpenAIProvider, ) +from plyngent.config.store import ConfigStore @pytest.fixture @@ -252,6 +254,34 @@ url = "https://example/v1" assert set(config.providers["local"].models) == {"base", "extra", "remote-a", "remote-b"} +def test_write_models_as_inline_tables(tmp_path: Path) -> None: + """Persisted models use inline tables, not dotted [providers.x.models.id] sections.""" + from plyngent.config import ModelConfig, OpenAICompatibleProvider + + path = tmp_path / "inline-models.toml" + config = ConfigStore(path=path, document=tomlkit.document()) + config.providers = { + "local": OpenAICompatibleProvider( + access_key_or_token="sk-test", + url="https://example/v1", + models={ + "base": ModelConfig(), + "gpt-test": ModelConfig(text=True, cost_factor=2), + }, + ) + } + config.write() + text = path.read_text(encoding="utf-8") + assert "[providers.local.models]" in text + assert "[providers.local.models.base]" not in text + assert "[providers.local.models.gpt-test]" not in text + assert "gpt-test" in text and "cost_factor" in text + # Round-trip still loads models. + again = plyngent.config.load(path) + assert set(again.providers["local"].models) == {"base", "gpt-test"} + assert again.providers["local"].models["gpt-test"].cost_factor == 2 + + def test_update_config() -> None: file = Path(__file__).parent / "plyngent-edit-2.toml" _ = shutil.copy(Path(__file__).parent / "plyngent-valid.toml", file)