runtime: route DeepSeek convention=anthropic to Anthropic client

This commit is contained in:
2026-08-01 02:31:21 +08:00
parent 439c24a1ce
commit a198fa9d41
4 changed files with 94 additions and 19 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
"""Convert agent chat history/tools to Anthropic Messages API shapes and back. """Convert agent chat history/tools to Anthropic Messages API shapes and back.
Agent memory and events stay chat-completions-shaped; only the transport uses Agent memory and events stay chat-completions-shaped; only the transport uses
``POST /messages``. OpenAI / DeepSeek paths never enter this module. ``POST /messages``. Real Anthropic and the DeepSeek ``convention="anthropic"``
surface both route through this module; OpenAI / openai-compatible paths never
enter it.
""" """
from __future__ import annotations from __future__ import annotations
+3 -1
View File
@@ -1,7 +1,9 @@
"""Anthropic Messages dispatch: present ``/messages`` as chat completions to the loop. """Anthropic Messages dispatch: present ``/messages`` as chat completions to the loop.
Agent memory and events stay chat-completions-shaped; only the transport uses Agent memory and events stay chat-completions-shaped; only the transport uses
``POST /messages``. OpenAI / DeepSeek paths never enter this module. ``POST /messages``. Real Anthropic and the DeepSeek ``convention="anthropic"``
surface both route through this module; OpenAI / openai-compatible paths never
enter it.
""" """
from __future__ import annotations from __future__ import annotations
+40 -15
View File
@@ -6,7 +6,11 @@ from typing import TYPE_CHECKING
from plyngent.config.routing import EffectiveProvider, resolve_effective_provider from plyngent.config.routing import EffectiveProvider, resolve_effective_provider
from plyngent.lmproto.anthropic import AnthropicClient from plyngent.lmproto.anthropic import AnthropicClient
from plyngent.lmproto.anthropic.config import AnthropicConfig as AnthropicConfigCls from plyngent.lmproto.anthropic.config import AnthropicConfig as AnthropicConfigCls
from plyngent.lmproto.deepseek import DeepseekOpenAIClient, DeepseekResponsesClient from plyngent.lmproto.deepseek import (
DeepseekAnthropicClient,
DeepseekOpenAIClient,
DeepseekResponsesClient,
)
from plyngent.lmproto.openai import OpenAIClient from plyngent.lmproto.openai import OpenAIClient
from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig
from plyngent.lmproto.openai_compatible.config import ( from plyngent.lmproto.openai_compatible.config import (
@@ -23,7 +27,12 @@ DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1"
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com" DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
type ProtocolClient = ( type ProtocolClient = (
OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient | DeepseekResponsesClient | AnthropicClient OpenAIClient
| OpenAICompatibleClient
| DeepseekOpenAIClient
| DeepseekResponsesClient
| DeepseekAnthropicClient
| AnthropicClient
) )
# Backward-compatible name used by older imports/tests. # Backward-compatible name used by older imports/tests.
type OpenAICompatibleClientUnion = ProtocolClient type OpenAICompatibleClientUnion = ProtocolClient
@@ -94,13 +103,37 @@ def provider_to_openai_config(
) )
def _anthropic_client(effective: EffectiveProvider) -> AnthropicClient:
"""Build an Anthropic Messages client (real or DeepSeek convention) for *effective*."""
try:
http_timeout = normalize_http_timeout(effective.timeout)
except InvalidHttpTimeoutError as exc:
raise ProviderNotSupportedError(str(exc)) from exc
if effective.preset == "deepseek":
return DeepseekAnthropicClient(
AnthropicConfigCls(
api_key=effective.access_key_or_token,
base_url=effective.url or DEFAULT_DEEPSEEK_BASE_URL,
timeout=http_timeout,
)
)
return AnthropicClient(
AnthropicConfigCls(
api_key=effective.access_key_or_token,
base_url=effective.url or DEFAULT_ANTHROPIC_BASE_URL,
timeout=http_timeout,
)
)
def create_client(provider: Provider, *, model: str | None = None) -> ProtocolClient: def create_client(provider: Provider, *, model: str | None = None) -> ProtocolClient:
"""Build a protocol client for *provider*, applying model-level routing. """Build a protocol client for *provider*, applying model-level routing.
``preset`` always decides API conventions: ``openai`` → /responses, ``preset`` always decides API conventions: ``openai`` → /responses,
``openai-compatible`` → /chat/completions, ``anthropic`` → /messages. ``openai-compatible`` → /chat/completions, ``anthropic`` → /messages.
Exception: a ``deepseek`` preset picks the Responses surface when its Exception: a ``deepseek`` preset picks the Responses or Anthropic surface
effective ``convention`` is ``"responses"``, else chat completions. when its effective ``convention`` is ``"responses"`` / ``"anthropic"``,
else chat completions.
""" """
effective = resolve_effective_provider(provider, model=model) effective = resolve_effective_provider(provider, model=model)
if effective.preset == "openai": if effective.preset == "openai":
@@ -110,18 +143,10 @@ def create_client(provider: Provider, *, model: str | None = None) -> ProtocolCl
if effective.preset == "deepseek": if effective.preset == "deepseek":
if effective.convention == "responses": if effective.convention == "responses":
return DeepseekResponsesClient(provider_to_openai_config(effective)) return DeepseekResponsesClient(provider_to_openai_config(effective))
if effective.convention == "anthropic":
return _anthropic_client(effective)
return DeepseekOpenAIClient(provider_to_openai_config(effective)) return DeepseekOpenAIClient(provider_to_openai_config(effective))
if effective.preset == "anthropic": if effective.preset == "anthropic":
try: return _anthropic_client(effective)
http_timeout = normalize_http_timeout(effective.timeout)
except InvalidHttpTimeoutError as exc:
raise ProviderNotSupportedError(str(exc)) from exc
return AnthropicClient(
AnthropicConfigCls(
api_key=effective.access_key_or_token,
base_url=effective.url or DEFAULT_ANTHROPIC_BASE_URL,
timeout=http_timeout,
)
)
msg = f"provider preset {effective.preset!r} is not supported" msg = f"provider preset {effective.preset!r} is not supported"
raise ProviderNotSupportedError(msg) raise ProviderNotSupportedError(msg)
+48 -2
View File
@@ -89,8 +89,54 @@ def test_deepseek_default_and_legacy_conventions_stay_chat() -> None:
default_client = create_client(DeepseekProvider(access_key_or_token="sk-test")) default_client = create_client(DeepseekProvider(access_key_or_token="sk-test"))
assert isinstance(default_client, DeepseekOpenAIClient) assert isinstance(default_client, DeepseekOpenAIClient)
assert not isinstance(default_client, DeepseekResponsesClient) assert not isinstance(default_client, DeepseekResponsesClient)
legacy = DeepseekProvider(access_key_or_token="sk-test", convention="anthropic") # extras.convention stays ignored (legacy location).
assert isinstance(create_client(legacy), DeepseekOpenAIClient) legacy_extra = DeepseekProvider(
access_key_or_token="sk-test",
extras={"convention": "anthropic"},
)
assert isinstance(create_client(legacy_extra), DeepseekOpenAIClient)
def test_deepseek_anthropic_convention() -> None:
from plyngent.lmproto.anthropic import AnthropicClient
from plyngent.lmproto.deepseek import DeepseekAnthropicClient
provider = DeepseekProvider(access_key_or_token="sk-test", convention="anthropic")
client = create_client(provider)
assert isinstance(client, DeepseekAnthropicClient)
assert isinstance(client, AnthropicClient)
assert client.kind == "messages"
assert client.session.base_url == "https://api.deepseek.com/anthropic"
def test_deepseek_anthropic_convention_custom_url() -> None:
from plyngent.lmproto.deepseek import DeepseekAnthropicClient
provider = DeepseekProvider(
access_key_or_token="sk-test",
url="https://gateway.example/anthropic",
convention="anthropic",
)
client = create_client(provider)
assert isinstance(client, DeepseekAnthropicClient)
assert client.session.base_url == "https://gateway.example/anthropic"
def test_deepseek_model_level_anthropic_convention() -> None:
from plyngent.lmproto.deepseek import DeepseekAnthropicClient, DeepseekOpenAIClient
provider = DeepseekProvider(
access_key_or_token="sk-test",
models={
"claude-proxy": ModelConfig(convention="anthropic"),
"deepseek-v4-flash": ModelConfig(),
},
)
proxy = create_client(provider, model="claude-proxy")
assert isinstance(proxy, DeepseekAnthropicClient)
assert proxy.session.base_url == "https://api.deepseek.com/anthropic"
flash = create_client(provider, model="deepseek-v4-flash")
assert isinstance(flash, DeepseekOpenAIClient)
def test_deepseek_model_level_convention_override() -> None: def test_deepseek_model_level_convention_override() -> None: