mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17: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.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Anthropic Messages dispatch: present ``/messages`` as chat completions to the loop.
|
||||
|
||||
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
|
||||
|
||||
@@ -6,7 +6,11 @@ from typing import TYPE_CHECKING
|
||||
from plyngent.config.routing import EffectiveProvider, resolve_effective_provider
|
||||
from plyngent.lmproto.anthropic import AnthropicClient
|
||||
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_compatible import OpenAICompatibleClient, OpenAIConfig
|
||||
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"
|
||||
|
||||
type ProtocolClient = (
|
||||
OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient | DeepseekResponsesClient | AnthropicClient
|
||||
OpenAIClient
|
||||
| OpenAICompatibleClient
|
||||
| DeepseekOpenAIClient
|
||||
| DeepseekResponsesClient
|
||||
| DeepseekAnthropicClient
|
||||
| AnthropicClient
|
||||
)
|
||||
# Backward-compatible name used by older imports/tests.
|
||||
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:
|
||||
"""Build a protocol client for *provider*, applying model-level routing.
|
||||
|
||||
``preset`` always decides API conventions: ``openai`` → /responses,
|
||||
``openai-compatible`` → /chat/completions, ``anthropic`` → /messages.
|
||||
Exception: a ``deepseek`` preset picks the Responses surface when its
|
||||
effective ``convention`` is ``"responses"``, else chat completions.
|
||||
Exception: a ``deepseek`` preset picks the Responses or Anthropic surface
|
||||
when its effective ``convention`` is ``"responses"`` / ``"anthropic"``,
|
||||
else chat completions.
|
||||
"""
|
||||
effective = resolve_effective_provider(provider, model=model)
|
||||
if effective.preset == "openai":
|
||||
@@ -110,18 +143,10 @@ def create_client(provider: Provider, *, model: str | None = None) -> ProtocolCl
|
||||
if effective.preset == "deepseek":
|
||||
if effective.convention == "responses":
|
||||
return DeepseekResponsesClient(provider_to_openai_config(effective))
|
||||
if effective.convention == "anthropic":
|
||||
return _anthropic_client(effective)
|
||||
return DeepseekOpenAIClient(provider_to_openai_config(effective))
|
||||
if effective.preset == "anthropic":
|
||||
try:
|
||||
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,
|
||||
)
|
||||
)
|
||||
return _anthropic_client(effective)
|
||||
msg = f"provider preset {effective.preset!r} is not supported"
|
||||
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"))
|
||||
assert isinstance(default_client, DeepseekOpenAIClient)
|
||||
assert not isinstance(default_client, DeepseekResponsesClient)
|
||||
legacy = DeepseekProvider(access_key_or_token="sk-test", convention="anthropic")
|
||||
assert isinstance(create_client(legacy), DeepseekOpenAIClient)
|
||||
# extras.convention stays ignored (legacy location).
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user