core/config: model-level preset and url overrides

This commit is contained in:
2026-07-28 21:42:48 +08:00
parent 0ca92dfd7e
commit 68025ca626
3 changed files with 126 additions and 1 deletions
+5
View File
@@ -20,6 +20,11 @@ from .models import PluginsConfig as PluginsConfig
from .models import ProviderConfig as ProviderConfig from .models import ProviderConfig as ProviderConfig
from .models import compose_agent_system_content as compose_agent_system_content from .models import compose_agent_system_content as compose_agent_system_content
from .path import get_default_path as _get_default_path 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 ConfigFormatError as ConfigFormatError
from .store import ConfigStore as ConfigStore from .store import ConfigStore as ConfigStore
+10 -1
View File
@@ -142,7 +142,13 @@ class NetworkingConfig(Struct, omit_defaults=True):
class ModelConfig(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 text: bool = True
image_in: bool = False image_in: bool = False
@@ -152,6 +158,8 @@ class ModelConfig(Struct, omit_defaults=True):
video_in: bool = False video_in: bool = False
video_out: bool = False video_out: bool = False
cost_factor: float = 1.0 cost_factor: float = 1.0
preset: str = ""
url: str = ""
class HttpTimeoutConfig(Struct, omit_defaults=True): class HttpTimeoutConfig(Struct, omit_defaults=True):
@@ -202,6 +210,7 @@ def _default_openai_provider_tools() -> list[dict[str, Any]]:
class OpenAIProvider(ProviderConfig, tag="openai"): class OpenAIProvider(ProviderConfig, tag="openai"):
"""OpenAI platform provider (agent uses Responses API). """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 ``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.
+111
View File
@@ -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,
}