mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
runtime: route DeepSeek convention=anthropic to Anthropic client
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user