mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
lmproto: add DeepSeek Anthropic client
This commit is contained in:
@@ -3,10 +3,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.lmproto.anthropic.client import AnthropicClient
|
from plyngent.lmproto.anthropic.client import AnthropicClient
|
||||||
from plyngent.lmproto.deepseek import DeepseekOpenAIClient, DeepseekResponsesClient
|
from plyngent.lmproto.deepseek import (
|
||||||
|
DeepseekAnthropicClient,
|
||||||
|
DeepseekOpenAIClient,
|
||||||
|
DeepseekResponsesClient,
|
||||||
|
)
|
||||||
from plyngent.lmproto.openai.client import OpenAIClient
|
from plyngent.lmproto.openai.client import OpenAIClient
|
||||||
from plyngent.lmproto.openai_compatible.client import OpenAICompatibleClient
|
from plyngent.lmproto.openai_compatible.client import OpenAICompatibleClient
|
||||||
|
|
||||||
type AnyLLMClient = (
|
type AnyLLMClient = (
|
||||||
OpenAICompatibleClient | DeepseekOpenAIClient | DeepseekResponsesClient | OpenAIClient | AnthropicClient
|
OpenAICompatibleClient
|
||||||
|
| DeepseekOpenAIClient
|
||||||
|
| DeepseekResponsesClient
|
||||||
|
| DeepseekAnthropicClient
|
||||||
|
| OpenAIClient
|
||||||
|
| AnthropicClient
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .anthropic.client import DeepseekAnthropicClient as DeepseekAnthropicClient
|
||||||
from .openai_compat.client import DeepseekOpenAIClient as DeepseekOpenAIClient
|
from .openai_compat.client import DeepseekOpenAIClient as DeepseekOpenAIClient
|
||||||
from .openai_compat.model import AnyChatMessage as AnyChatMessage
|
from .openai_compat.model import AnyChatMessage as AnyChatMessage
|
||||||
from .openai_compat.model import AssistantChatMessage as AssistantChatMessage
|
from .openai_compat.model import AssistantChatMessage as AssistantChatMessage
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
from .client import DeepseekAnthropicClient as DeepseekAnthropicClient
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""DeepSeek Anthropic Messages client (``POST /messages`` on the DeepSeek endpoint)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, override
|
||||||
|
|
||||||
|
from plyngent.lmproto.anthropic.client import AnthropicClient
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from plyngent.lmproto.anthropic.config import AnthropicConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DeepseekAnthropicClient(AnthropicClient):
|
||||||
|
"""Anthropic Messages client pointed at DeepSeek's Anthropic-compatible endpoint.
|
||||||
|
|
||||||
|
DeepSeek exposes the Anthropic API format at ``https://api.deepseek.com/anthropic``
|
||||||
|
(see https://api-docs.deepseek.com/guides/anthropic_api). ``x-api-key`` is fully
|
||||||
|
supported; ``anthropic-version`` / ``anthropic-beta`` headers are ignored by the
|
||||||
|
server but harmless. Model ids map server-side: ``claude-opus*`` → ``deepseek-v4-pro``,
|
||||||
|
``claude-haiku*`` / ``claude-sonnet*`` → ``deepseek-v4-flash``, unknown names →
|
||||||
|
``deepseek-v4-flash``; real DeepSeek ids pass through as-is.
|
||||||
|
|
||||||
|
``GET /models`` is not documented on the ``/anthropic`` base, so ``models()``
|
||||||
|
returns an empty list and model selection stays config-driven.
|
||||||
|
"""
|
||||||
|
|
||||||
|
kind: str = "messages"
|
||||||
|
|
||||||
|
def __init__(self, config: AnthropicConfig) -> None:
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def models(self) -> list[str]:
|
||||||
|
"""No documented ``GET /models`` on DeepSeek's ``/anthropic`` base."""
|
||||||
|
return []
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plyngent.lmproto.anthropic.config import AnthropicConfig
|
||||||
|
from plyngent.lmproto.anthropic.model import (
|
||||||
|
AnthropicMessageResponse,
|
||||||
|
AnthropicMessagesParam,
|
||||||
|
AnthropicResponseText,
|
||||||
|
AnthropicUsage,
|
||||||
|
)
|
||||||
|
from plyngent.lmproto.deepseek import DeepseekAnthropicClient
|
||||||
|
|
||||||
|
|
||||||
|
def _sample_message_body() -> bytes:
|
||||||
|
return msgspec.json.encode(
|
||||||
|
AnthropicMessageResponse(
|
||||||
|
id="msg_1",
|
||||||
|
model="deepseek-v4-flash",
|
||||||
|
content=[AnthropicResponseText(text="hello world")],
|
||||||
|
stop_reason="end_turn",
|
||||||
|
usage=AnthropicUsage(input_tokens=9, output_tokens=3),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_kind_and_base_url() -> None:
|
||||||
|
client = DeepseekAnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://api.deepseek.com/anthropic"))
|
||||||
|
assert client.kind == "messages"
|
||||||
|
assert client.session.base_url == "https://api.deepseek.com/anthropic"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_messages_create(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
client = DeepseekAnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://api.deepseek.com/anthropic"))
|
||||||
|
|
||||||
|
class _Resp:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self) -> bytes:
|
||||||
|
return _sample_message_body()
|
||||||
|
|
||||||
|
async def fake_post(path: str, **kwargs: object) -> _Resp:
|
||||||
|
assert path == "/messages"
|
||||||
|
assert kwargs.get("stream") is False
|
||||||
|
headers = kwargs.get("headers")
|
||||||
|
assert isinstance(headers, dict)
|
||||||
|
assert headers.get("x-api-key") == "sk-test"
|
||||||
|
assert "anthropic-version" in headers
|
||||||
|
return _Resp()
|
||||||
|
|
||||||
|
monkeypatch.setattr(client.session, "post", fake_post)
|
||||||
|
result = await client.messages(AnthropicMessagesParam(model="deepseek-v4-flash", messages=[]))
|
||||||
|
assert isinstance(result, AnthropicMessageResponse)
|
||||||
|
assert result.content[0].text == "hello world"
|
||||||
|
assert result.usage.output_tokens == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_messages_stream(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
client = DeepseekAnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://api.deepseek.com/anthropic"))
|
||||||
|
|
||||||
|
class _Resp:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
async def iter_lines(self):
|
||||||
|
yield b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hel"}}'
|
||||||
|
yield b'data: {"type":"message_stop"}'
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def fake_post(path: str, **kwargs: object) -> _Resp:
|
||||||
|
assert path == "/messages"
|
||||||
|
assert kwargs.get("stream") is True
|
||||||
|
return _Resp()
|
||||||
|
|
||||||
|
monkeypatch.setattr(client.session, "post", fake_post)
|
||||||
|
stream = await client.messages(AnthropicMessagesParam(model="deepseek-v4-flash", messages=[]), stream=True)
|
||||||
|
events = [event async for event in stream]
|
||||||
|
assert len(events) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_models_empty_without_network(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
client = DeepseekAnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://api.deepseek.com/anthropic"))
|
||||||
|
|
||||||
|
async def fail_get(*_args: object, **_kwargs: object) -> object:
|
||||||
|
msg = "GET /models must not be called on the /anthropic base"
|
||||||
|
raise AssertionError(msg)
|
||||||
|
|
||||||
|
monkeypatch.setattr(client.session, "get", fail_get)
|
||||||
|
assert await client.models() == []
|
||||||
Reference in New Issue
Block a user