From c7ee9b006354f1a8b45f0f7fc28ef5e2528d569d Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 28 Jul 2026 21:45:59 +0800 Subject: [PATCH] test/config: cover model-level preset routing --- doc/architecture.md | 3 +- doc/plyngent.example.toml | 4 +++ tests/test_cli/test_oneshot.py | 4 +-- tests/test_config/test_config.py | 24 ++++++++++++++++ tests/test_runtime/test_client_factory.py | 35 +++++++++++++++++++++-- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index cbbae20..ba60b69 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -20,7 +20,8 @@ - memory: Storage controlling for sessions and messages. - router: Multi-source capability routing (Phase H; not implemented). - config: Plyngent configuration center (TOML), including ``[plugins]`` and - ``[networking]`` (e.g. fetch SSRF Fake-IP CIDR exemptions). + ``[networking]`` (e.g. fetch SSRF Fake-IP CIDR exemptions). Model entries + may override provider ``preset`` / ``url`` for mixed API routing. - agent: Tool loop, streaming, usage, compact; `@tool` / tags / registry. - tools: Workspace file/process/VCS/chat/todo/net tools; catalog; plugins; instance/session context and views. diff --git a/doc/plyngent.example.toml b/doc/plyngent.example.toml index 30bb1ac..7763861 100644 --- a/doc/plyngent.example.toml +++ b/doc/plyngent.example.toml @@ -87,6 +87,10 @@ access_key_or_token = "sk-replace-me" # # [providers.openai.models] # "gpt-4o-mini" = { text = true } +# # Model-level preset/url overrides let one provider route models via different APIs. +# # `preset = "openai"` means /responses; `openai-compatible` means /chat/completions; +# # `anthropic` means native /messages (support planned). +# # "claude-sonnet" = { text = true, preset = "anthropic", url = "https://api.anthropic.com/v1" } # --- DeepSeek (OpenAI-compatible convention) --- # Default models if [providers.*.models] is omitted: deepseek-v4-flash, deepseek-v4-pro. diff --git a/tests/test_cli/test_oneshot.py b/tests/test_cli/test_oneshot.py index eca809d..9dbccdb 100644 --- a/tests/test_cli/test_oneshot.py +++ b/tests/test_cli/test_oneshot.py @@ -129,10 +129,10 @@ access_key_or_token = "sk" usage={}, ) - monkeypatch.setattr("plyngent.cli.app.create_client", lambda _p: DummyClient()) + monkeypatch.setattr("plyngent.cli.app.create_client", lambda _p, **_kw: DummyClient()) monkeypatch.setattr( "plyngent.cli.state.create_client", - lambda _p: DummyClient(), + lambda _p, **_kw: DummyClient(), ) from platformdirs import user_data_path diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index 4067dc5..777baa0 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -189,6 +189,30 @@ models = {} _ = config.promote_provider("hollow", []) +def test_model_level_preset_url_overrides_parse(tmp_path: Path) -> None: + path = tmp_path / "mixed.toml" + _ = path.write_text( + """ +[providers.mixed] +preset = "openai-compatible" +url = "https://gateway.example/v1" +access_key_or_token = "sk-test" + +[providers.mixed.models] +"claude" = { text = true, preset = "anthropic", url = "https://api.anthropic.com/v1" } +"qwen" = { text = true } +""", + encoding="utf-8", + ) + config = plyngent.config.load(path) + provider = config.providers["mixed"] + assert isinstance(provider, OpenAICompatibleProvider) + assert provider.models["claude"].preset == "anthropic" + assert provider.models["claude"].url == "https://api.anthropic.com/v1" + assert provider.models["qwen"].preset == "" + assert provider.models["qwen"].url == "" + + def test_read_invalid_config() -> None: with pytest.raises(ConfigFormatError): _ = plyngent.config.load(Path(__file__).parent / "plyngent-invalid.toml") diff --git a/tests/test_runtime/test_client_factory.py b/tests/test_runtime/test_client_factory.py index b8f199b..b1d4a35 100644 --- a/tests/test_runtime/test_client_factory.py +++ b/tests/test_runtime/test_client_factory.py @@ -5,6 +5,7 @@ import pytest from plyngent.config.models import ( AnthropicProvider, DeepseekProvider, + ModelConfig, OpenAICompatibleProvider, OpenAIProvider, ) @@ -65,16 +66,44 @@ def test_deepseek_openai_convention() -> None: assert "deepseek-v4-pro" in provider.models -def test_deepseek_anthropic_convention_not_implemented() -> None: +def test_deepseek_ignores_legacy_convention_extra() -> None: provider = DeepseekProvider( access_key_or_token="sk-test", extras={"convention": "anthropic"}, ) - with pytest.raises(ProviderNotSupportedError, match="anthropic"): - _ = create_client(provider) + client = create_client(provider) + assert isinstance(client, DeepseekOpenAIClient) def test_anthropic_not_implemented() -> None: provider = AnthropicProvider(access_key_or_token="sk-test") with pytest.raises(ProviderNotSupportedError, match="anthropic"): _ = create_client(provider) + + +def test_model_level_preset_url_overrides_parent() -> None: + provider = OpenAICompatibleProvider( + access_key_or_token="sk-test", + url="https://gateway.example/v1", + models={ + "gpt": ModelConfig(preset="openai", url="https://api.openai.com/v1"), + "qwen": ModelConfig(), + }, + ) + openai_client = create_client(provider, model="gpt") + assert openai_client.__class__.__name__ == "ResponsesChatClient" + assert provider_to_openai_config(provider, model="gpt").base_url == "https://api.openai.com/v1" + + compat_client = create_client(provider, model="qwen") + assert isinstance(compat_client, OpenAICompatibleClient) + assert provider_to_openai_config(provider, model="qwen").base_url == "https://gateway.example/v1" + + +def test_model_level_anthropic_preset_not_implemented() -> None: + provider = OpenAICompatibleProvider( + access_key_or_token="sk-test", + url="https://gateway.example/v1", + models={"claude": ModelConfig(preset="anthropic", url="https://api.anthropic.com/v1")}, + ) + with pytest.raises(ProviderNotSupportedError, match="anthropic"): + _ = create_client(provider, model="claude")