From 68025ca626876313f257f06efe1c8db29f0107b7 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 28 Jul 2026 21:42:48 +0800 Subject: [PATCH] core/config: model-level preset and url overrides --- src/plyngent/config/__init__.py | 5 ++ src/plyngent/config/models.py | 11 +++- src/plyngent/config/routing.py | 111 ++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/plyngent/config/routing.py diff --git a/src/plyngent/config/__init__.py b/src/plyngent/config/__init__.py index a00074c..e0ea3ee 100644 --- a/src/plyngent/config/__init__.py +++ b/src/plyngent/config/__init__.py @@ -20,6 +20,11 @@ from .models import PluginsConfig as PluginsConfig from .models import ProviderConfig as ProviderConfig from .models import compose_agent_system_content as compose_agent_system_content from .path import get_default_path as _get_default_path +from .routing import EffectiveProvider as EffectiveProvider +from .routing import ProviderPreset as ProviderPreset +from .routing import default_url_for_preset as default_url_for_preset +from .routing import provider_preset as provider_preset +from .routing import resolve_effective_provider as resolve_effective_provider from .store import ConfigFormatError as ConfigFormatError from .store import ConfigStore as ConfigStore diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index 0fe9f65..7d6c331 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -142,7 +142,13 @@ class NetworkingConfig(Struct, omit_defaults=True): class ModelConfig(Struct, omit_defaults=True): - """Capability flags for a model within a provider.""" + """Capability flags and optional routing overrides for a model. + + ``preset`` / ``url`` override the parent provider for this model only. + ``preset`` keeps the same meanings as provider presets: + ``openai`` → /responses, ``openai-compatible`` → /chat/completions, + ``anthropic`` → /messages (native support planned), ``deepseek`` → DeepSeek. + """ text: bool = True image_in: bool = False @@ -152,6 +158,8 @@ class ModelConfig(Struct, omit_defaults=True): video_in: bool = False video_out: bool = False cost_factor: float = 1.0 + preset: str = "" + url: str = "" class HttpTimeoutConfig(Struct, omit_defaults=True): @@ -202,6 +210,7 @@ def _default_openai_provider_tools() -> list[dict[str, Any]]: class OpenAIProvider(ProviderConfig, tag="openai"): """OpenAI platform provider (agent uses Responses API). + ``url`` should include the API version path (default: ``https://api.openai.com/v1``). ``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. diff --git a/src/plyngent/config/routing.py b/src/plyngent/config/routing.py new file mode 100644 index 0000000..4be7124 --- /dev/null +++ b/src/plyngent/config/routing.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal, cast + +from .models import ( + AnthropicProvider, + HttpTimeoutConfig, + ModelConfig, + OpenAICompatibleProvider, + OpenAIProvider, + Provider, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + + +type ProviderPreset = Literal["openai", "openai-compatible", "anthropic", "deepseek"] + + +@dataclass(frozen=True, slots=True) +class EffectiveProvider: + """Provider routing after applying model-level ``preset`` / ``url`` overrides.""" + + preset: ProviderPreset + access_key_or_token: str + url: str + model: str | None + timeout: float | HttpTimeoutConfig | None + provider_tools: list[dict[str, object]] + source: Provider + + +def provider_preset(provider: Provider) -> ProviderPreset: + """Return the tagged-union preset for *provider*.""" + if isinstance(provider, OpenAIProvider): + return "openai" + if isinstance(provider, OpenAICompatibleProvider): + return "openai-compatible" + if isinstance(provider, AnthropicProvider): + return "anthropic" + return "deepseek" + + +def default_url_for_preset(preset: ProviderPreset) -> str: + """Default base URL; includes the API version path when applicable.""" + if preset == "openai": + return "https://api.openai.com/v1" + if preset == "anthropic": + return "https://api.anthropic.com/v1" + if preset == "deepseek": + return "https://api.deepseek.com" + # Generic OpenAI-compatible providers usually require a host-specific URL. + return "" + + +def validate_model_preset(preset: str) -> ProviderPreset: + if preset in {"openai", "openai-compatible", "anthropic", "deepseek"}: + return cast("ProviderPreset", preset) + msg = f"unsupported model preset {preset!r}" + raise ValueError(msg) + + +def model_config_for(provider: Provider, model: str | None) -> ModelConfig | None: + if not model: + return None + return provider.models.get(model) + + +def resolve_effective_provider(provider: Provider, *, model: str | None = None) -> EffectiveProvider: + """Apply model-level preset/url overrides to *provider*. + + ``preset`` always decides API conventions: ``openai`` means Responses, + ``openai-compatible`` means Chat Completions, ``anthropic`` means Messages. + """ + parent_preset = provider_preset(provider) + model_cfg = model_config_for(provider, model) + if model_cfg is not None and model_cfg.preset.strip(): + preset = validate_model_preset(model_cfg.preset.strip()) + else: + preset = parent_preset + url = model_cfg.url.strip() if model_cfg is not None and model_cfg.url.strip() else provider.url.strip() + if not url: + url = default_url_for_preset(preset) + + provider_tools: list[dict[str, object]] = [] + if preset == "openai" and isinstance(provider, OpenAIProvider) and parent_preset == "openai": + provider_tools = [dict(item) for item in provider.provider_tools] + + return EffectiveProvider( + preset=preset, + access_key_or_token=provider.access_key_or_token, + url=url, + model=model, + timeout=provider.timeout, + provider_tools=provider_tools, + source=provider, + ) + + +def effective_provider_to_config_dict(effective: EffectiveProvider) -> Mapping[str, object]: + """Debug/status-friendly dict for an effective provider.""" + return { + "preset": effective.preset, + "access_key_or_token": effective.access_key_or_token, + "url": effective.url, + "model": effective.model or "", + "timeout": effective.timeout, + "provider_tools": effective.provider_tools, + }