config: default deepseek anthropic convention url

This commit is contained in:
2026-08-01 02:27:23 +08:00
parent f6072ce2a6
commit 6e4d229f6f
3 changed files with 73 additions and 14 deletions
+5 -3
View File
@@ -6,7 +6,8 @@ from msgspec import Struct, field
# per-model ``ModelConfig.convention`` override). # per-model ``ModelConfig.convention`` override).
# - "" (default) / "openai" → OpenAI-compatible chat completions # - "" (default) / "openai" → OpenAI-compatible chat completions
# - "responses" → OpenAI Responses API (``POST /responses``) # - "responses" → OpenAI Responses API (``POST /responses``)
# - "anthropic" → legacy value, ignored (still chat completions) # - "anthropic" → Anthropic Messages API (``POST /messages`` on
# ``https://api.deepseek.com/anthropic``)
type DeepSeekConvention = Literal["", "openai", "anthropic", "responses"] type DeepSeekConvention = Literal["", "openai", "anthropic", "responses"]
# Built-in persona when ``[agent].system_prompt`` is omitted. # Built-in persona when ``[agent].system_prompt`` is omitted.
@@ -255,10 +256,11 @@ def _default_deepseek_models() -> dict[str, ModelConfig]:
class DeepseekProvider(ProviderConfig, tag="deepseek"): class DeepseekProvider(ProviderConfig, tag="deepseek"):
"""DeepSeek API provider. """DeepSeek API provider.
``convention`` selects the API surface on ``https://api.deepseek.com``: ``convention`` selects the API surface on DeepSeek's endpoints:
- ``""`` / ``"openai"`` → OpenAI-compatible chat completions (default) - ``""`` / ``"openai"`` → OpenAI-compatible chat completions (default)
- ``"responses"`` → OpenAI Responses API (``POST /responses``) - ``"responses"`` → OpenAI Responses API (``POST /responses``)
- ``"anthropic"`` → legacy value, ignored (still chat completions) - ``"anthropic"`` → Anthropic Messages API (``POST /messages`` on
``https://api.deepseek.com/anthropic``)
``extras`` keeps arbitrary provider keys; the legacy ``extras.convention`` ``extras`` keeps arbitrary provider keys; the legacy ``extras.convention``
key is parsed but ignored (prefer the typed ``convention`` field). key is parsed but ignored (prefer the typed ``convention`` field).
+29 -11
View File
@@ -19,6 +19,9 @@ if TYPE_CHECKING:
type ProviderPreset = Literal["openai", "openai-compatible", "anthropic", "deepseek"] type ProviderPreset = Literal["openai", "openai-compatible", "anthropic", "deepseek"]
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL = "https://api.deepseek.com/anthropic"
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class EffectiveProvider: class EffectiveProvider:
@@ -54,11 +57,22 @@ def default_url_for_preset(preset: ProviderPreset) -> str:
if preset == "anthropic": if preset == "anthropic":
return "https://api.anthropic.com/v1" return "https://api.anthropic.com/v1"
if preset == "deepseek": if preset == "deepseek":
return "https://api.deepseek.com" return DEFAULT_DEEPSEEK_BASE_URL
# Generic OpenAI-compatible providers usually require a host-specific URL. # Generic OpenAI-compatible providers usually require a host-specific URL.
return "" return ""
def _default_url_for_deepseek_convention(convention: str) -> str:
"""Default DeepSeek base URL for an API convention.
The Anthropic-convention endpoint lives under ``/anthropic``; chat and
Responses share the plain ``https://api.deepseek.com`` base.
"""
if convention == "anthropic":
return DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
return DEFAULT_DEEPSEEK_BASE_URL
def validate_model_preset(preset: str) -> ProviderPreset: def validate_model_preset(preset: str) -> ProviderPreset:
if preset in {"openai", "openai-compatible", "anthropic", "deepseek"}: if preset in {"openai", "openai-compatible", "anthropic", "deepseek"}:
return cast("ProviderPreset", preset) return cast("ProviderPreset", preset)
@@ -77,9 +91,9 @@ def resolve_effective_provider(provider: Provider, *, model: str | None = None)
``preset`` always decides API conventions: ``openai`` means Responses, ``preset`` always decides API conventions: ``openai`` means Responses,
``openai-compatible`` means Chat Completions, ``anthropic`` means Messages. ``openai-compatible`` means Chat Completions, ``anthropic`` means Messages.
Exception: a ``deepseek`` preset may pick the Responses surface via the Exception: a ``deepseek`` preset may pick the Responses or Anthropic
``convention`` field (``"responses"``); model-level ``convention`` wins surface via the ``convention`` field (``"responses"`` / ``"anthropic"``);
over the provider-level one. model-level ``convention`` wins over the provider-level one.
""" """
parent_preset = provider_preset(provider) parent_preset = provider_preset(provider)
model_cfg = model_config_for(provider, model) model_cfg = model_config_for(provider, model)
@@ -87,13 +101,6 @@ def resolve_effective_provider(provider: Provider, *, model: str | None = None)
preset = validate_model_preset(model_cfg.preset.strip()) preset = validate_model_preset(model_cfg.preset.strip())
else: else:
preset = parent_preset 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]
# DeepSeek API surface: model override > provider convention > chat default. # DeepSeek API surface: model override > provider convention > chat default.
convention = "" convention = ""
@@ -102,6 +109,17 @@ def resolve_effective_provider(provider: Provider, *, model: str | None = None)
if model_cfg is not None and model_cfg.convention: if model_cfg is not None and model_cfg.convention:
convention = model_cfg.convention convention = model_cfg.convention
url = model_cfg.url.strip() if model_cfg is not None and model_cfg.url.strip() else provider.url.strip()
if not url:
if preset == "deepseek":
url = _default_url_for_deepseek_convention(convention)
else:
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( return EffectiveProvider(
preset=preset, preset=preset,
access_key_or_token=provider.access_key_or_token, access_key_or_token=provider.access_key_or_token,
+39
View File
@@ -247,6 +247,45 @@ def test_deepseek_effective_convention_precedence() -> None:
assert resolve_effective_provider(OpenAIProvider(access_key_or_token="sk")).convention == "" assert resolve_effective_provider(OpenAIProvider(access_key_or_token="sk")).convention == ""
def test_deepseek_anthropic_convention_default_url() -> None:
from plyngent.config import resolve_effective_provider
from plyngent.config.routing import DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL, DEFAULT_DEEPSEEK_BASE_URL
provider = DeepseekProvider(access_key_or_token="sk-test", convention="anthropic")
effective = resolve_effective_provider(provider)
assert effective.convention == "anthropic"
assert effective.url == DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
# Custom provider url wins over the convention default.
provider = DeepseekProvider(
access_key_or_token="sk-test",
url="https://gateway.example/anthropic",
convention="anthropic",
)
assert resolve_effective_provider(provider).url == "https://gateway.example/anthropic"
# Other conventions keep the plain deepseek base.
for convention in ("", "openai", "responses"):
provider = DeepseekProvider(access_key_or_token="sk-test", convention=convention)
assert resolve_effective_provider(provider).url == DEFAULT_DEEPSEEK_BASE_URL
def test_deepseek_anthropic_model_convention_default_url() -> None:
from plyngent.config import ModelConfig, resolve_effective_provider
from plyngent.config.routing import DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
provider = DeepseekProvider(
access_key_or_token="sk-test",
models={"claude-proxy": ModelConfig(convention="anthropic")},
)
effective = resolve_effective_provider(provider, model="claude-proxy")
assert effective.convention == "anthropic"
assert effective.url == DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
# Provider default url for a model without convention stays the deepseek base.
other = resolve_effective_provider(provider, model=None)
assert other.url == "https://api.deepseek.com"
def test_read_empty_config() -> None: def test_read_empty_config() -> None:
config = plyngent.config.load(Path(__file__).parent / "plyngent-empty.toml") config = plyngent.config.load(Path(__file__).parent / "plyngent-empty.toml")
assert isinstance(config.providers, Mapping) assert isinstance(config.providers, Mapping)