diff --git a/README.md b/README.md index 45a2dfa..5e455bf 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ confirm_destructive = true max_context_tokens = 200000 ``` -Supported provider presets today: `openai`, `openai-compatible`, `deepseek` (OpenAI convention; default models `deepseek-v4-flash` and `deepseek-v4-pro` if `models` is omitted). Anthropic presets are modeled in config but not wired in the runtime client yet. +Supported provider presets today: `openai` (default if `preset` is omitted; default models `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.4-nano` when `models` is omitted), `openai-compatible`, `deepseek` (OpenAI convention; default models `deepseek-v4-flash` and `deepseek-v4-pro` if `models` is omitted). Anthropic presets are modeled in config but not wired in the runtime client yet. If `[database]` is omitted (or SQLite `url` is empty/`":memory:"`), chat uses a durable file under the user data dir (e.g. `~/.local/share/plyngent/chat.db` on Linux). diff --git a/doc/plyngent.example.toml b/doc/plyngent.example.toml index d6b3180..fa5926f 100644 --- a/doc/plyngent.example.toml +++ b/doc/plyngent.example.toml @@ -32,6 +32,8 @@ access_key_or_token = "sk-replace-me" "gpt-4o-mini" = { text = true, cost_factor = 1.0 } # --- OpenAI platform (Responses API; tag is "openai") --- +# If ``preset`` is omitted, it defaults to "openai". +# If ``models`` is omitted, defaults: gpt-5.4, gpt-5.4-mini, gpt-5.4-nano. # [providers.openai] # preset = "openai" # access_key_or_token = "sk-replace-me" diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index d844da4..48e84f3 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -53,14 +53,27 @@ class ProviderConfig(Struct, tag_field="preset", omit_defaults=True): models: dict[str, ModelConfig] = field(default_factory=dict) +def _default_openai_models() -> dict[str, ModelConfig]: + """Current OpenAI text catalog when TOML omits ``models``.""" + return { + "gpt-5.4": ModelConfig(text=True), + "gpt-5.4-mini": ModelConfig(text=True), + "gpt-5.4-nano": ModelConfig(text=True), + } + + class OpenAIProvider(ProviderConfig, tag="openai"): """OpenAI platform provider (agent uses Responses API). ``provider_tools`` are hosted/provider-side tools (e.g. web_search) passed through to ``POST /responses`` as opaque dicts. They are **not** local ``ToolRegistry`` handlers and are ignored by non-OpenAI clients. + + When ``models`` is omitted in TOML, seeds a small default catalog (same idea + as DeepSeek). Explicit ``models = {}`` stays empty (recoverable / free-form). """ + models: dict[str, ModelConfig] = field(default_factory=_default_openai_models) provider_tools: list[dict[str, Any]] = field(default_factory=list) diff --git a/src/plyngent/config/store.py b/src/plyngent/config/store.py index 3c071b8..ec2fcf4 100644 --- a/src/plyngent/config/store.py +++ b/src/plyngent/config/store.py @@ -60,9 +60,14 @@ def _parse_providers( bad_providers[name] = raw_entry continue + entry = cast("dict[str, object]", raw_entry) + # Missing preset → OpenAI platform (Responses-capable defaults). + if "preset" not in entry: + entry = {**entry, "preset": "openai"} + # Try tagged-union dispatch via msgspec try: - provider: Provider = msgspec.convert(raw_entry, Provider) + provider: Provider = msgspec.convert(entry, Provider) except msgspec.ValidationError: bad_providers[name] = raw_entry continue @@ -72,12 +77,13 @@ def _parse_providers( cls = type(provider) known = set(cls.__struct_fields__) known.add("preset") # tag_field, excluded from __struct_fields__ - if set(cast("dict[str, object]", raw_entry).keys()) - known: + # Validate against the entry we actually converted (may have injected preset). + if set(entry.keys()) - known: bad_providers[name] = raw_entry continue - # Empty models: keep as recoverable (DeepSeek seeds defaults when - # models is omitted, so only explicit models={} lands here). + # Empty models: recoverable. OpenAI/DeepSeek seed defaults when + # ``models`` is omitted; only explicit models={} lands here. if not provider.models: recoverable[name] = provider continue diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index 9944e69..92ae82d 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -58,6 +58,27 @@ def test_deepseek_default_models_on_construct() -> None: assert set(provider.models) == {"deepseek-v4-flash", "deepseek-v4-pro"} +def test_openai_default_models_on_construct() -> None: + provider = OpenAIProvider(access_key_or_token="sk-test") + assert set(provider.models) == {"gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"} + + +def test_openai_omitted_preset_and_models_from_toml(tmp_path: Path) -> None: + path = tmp_path / "openai-defaults.toml" + _ = path.write_text( + """ +[providers.oai] +access_key_or_token = "sk-test" +""", + encoding="utf-8", + ) + config = plyngent.config.load(path) + assert "oai" in config.providers + provider = config.providers["oai"] + assert isinstance(provider, OpenAIProvider) + assert set(provider.models) == {"gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"} + + def test_deepseek_explicit_models_override_defaults() -> None: from plyngent.config import ModelConfig