core/config: default OpenAI preset and model catalog

Missing preset becomes openai; omitted models seed gpt-5.4 / mini / nano.
This commit is contained in:
2026-07-15 21:40:43 +08:00
parent 16992be743
commit 84a83e0485
5 changed files with 47 additions and 5 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ confirm_destructive = true
max_context_tokens = 200000 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). 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).
+2
View File
@@ -32,6 +32,8 @@ access_key_or_token = "sk-replace-me"
"gpt-4o-mini" = { text = true, cost_factor = 1.0 } "gpt-4o-mini" = { text = true, cost_factor = 1.0 }
# --- OpenAI platform (Responses API; tag is "openai") --- # --- 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] # [providers.openai]
# preset = "openai" # preset = "openai"
# access_key_or_token = "sk-replace-me" # access_key_or_token = "sk-replace-me"
+13
View File
@@ -53,14 +53,27 @@ class ProviderConfig(Struct, tag_field="preset", omit_defaults=True):
models: dict[str, ModelConfig] = field(default_factory=dict) 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"): class OpenAIProvider(ProviderConfig, tag="openai"):
"""OpenAI platform provider (agent uses Responses API). """OpenAI platform provider (agent uses Responses API).
``provider_tools`` are hosted/provider-side tools (e.g. web_search) passed ``provider_tools`` are hosted/provider-side tools (e.g. web_search) passed
through to ``POST /responses`` as opaque dicts. They are **not** local through to ``POST /responses`` as opaque dicts. They are **not** local
``ToolRegistry`` handlers and are ignored by non-OpenAI clients. ``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) provider_tools: list[dict[str, Any]] = field(default_factory=list)
+10 -4
View File
@@ -60,9 +60,14 @@ def _parse_providers(
bad_providers[name] = raw_entry bad_providers[name] = raw_entry
continue 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 tagged-union dispatch via msgspec
try: try:
provider: Provider = msgspec.convert(raw_entry, Provider) provider: Provider = msgspec.convert(entry, Provider)
except msgspec.ValidationError: except msgspec.ValidationError:
bad_providers[name] = raw_entry bad_providers[name] = raw_entry
continue continue
@@ -72,12 +77,13 @@ def _parse_providers(
cls = type(provider) cls = type(provider)
known = set(cls.__struct_fields__) known = set(cls.__struct_fields__)
known.add("preset") # tag_field, excluded from __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 bad_providers[name] = raw_entry
continue continue
# Empty models: keep as recoverable (DeepSeek seeds defaults when # Empty models: recoverable. OpenAI/DeepSeek seed defaults when
# models is omitted, so only explicit models={} lands here). # ``models`` is omitted; only explicit models={} lands here.
if not provider.models: if not provider.models:
recoverable[name] = provider recoverable[name] = provider
continue continue
+21
View File
@@ -58,6 +58,27 @@ def test_deepseek_default_models_on_construct() -> None:
assert set(provider.models) == {"deepseek-v4-flash", "deepseek-v4-pro"} 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: def test_deepseek_explicit_models_override_defaults() -> None:
from plyngent.config import ModelConfig from plyngent.config import ModelConfig