diff --git a/src/plyngent/runtime/client_factory.py b/src/plyngent/runtime/client_factory.py index 826dbcf..3e12050 100644 --- a/src/plyngent/runtime/client_factory.py +++ b/src/plyngent/runtime/client_factory.py @@ -6,7 +6,7 @@ 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 +from plyngent.lmproto.deepseek import DeepseekOpenAIClient, DeepseekResponsesClient from plyngent.lmproto.openai import OpenAIClient from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig from plyngent.lmproto.openai_compatible.config import ( @@ -22,7 +22,9 @@ DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1" DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1" DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com" -type ProtocolClient = OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient | AnthropicClient +type ProtocolClient = ( + OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient | DeepseekResponsesClient | AnthropicClient +) # Backward-compatible name used by older imports/tests. type OpenAICompatibleClientUnion = ProtocolClient @@ -97,6 +99,8 @@ def create_client(provider: Provider, *, model: str | None = None) -> ProtocolCl ``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. """ effective = resolve_effective_provider(provider, model=model) if effective.preset == "openai": @@ -104,6 +108,8 @@ def create_client(provider: Provider, *, model: str | None = None) -> ProtocolCl if effective.preset == "openai-compatible": return OpenAICompatibleClient(provider_to_openai_config(effective)) if effective.preset == "deepseek": + if effective.convention == "responses": + return DeepseekResponsesClient(provider_to_openai_config(effective)) return DeepseekOpenAIClient(provider_to_openai_config(effective)) if effective.preset == "anthropic": try: diff --git a/tests/test_runtime/test_client_factory.py b/tests/test_runtime/test_client_factory.py index a05e901..fc55507 100644 --- a/tests/test_runtime/test_client_factory.py +++ b/tests/test_runtime/test_client_factory.py @@ -73,6 +73,43 @@ def test_deepseek_ignores_legacy_convention_extra() -> None: assert isinstance(client, DeepseekOpenAIClient) +def test_deepseek_responses_convention() -> None: + from plyngent.lmproto.deepseek import DeepseekResponsesClient + + provider = DeepseekProvider(access_key_or_token="sk-test", convention="responses") + client = create_client(provider) + assert isinstance(client, DeepseekResponsesClient) + assert client.kind == "responses" + assert provider_to_openai_config(provider).base_url == "https://api.deepseek.com" + + +def test_deepseek_default_and_legacy_conventions_stay_chat() -> None: + from plyngent.lmproto.deepseek import DeepseekResponsesClient + + 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) + + +def test_deepseek_model_level_convention_override() -> None: + from plyngent.lmproto.deepseek import DeepseekResponsesClient + + provider = DeepseekProvider( + access_key_or_token="sk-test", + models={ + "deepseek-v4-flash": ModelConfig(convention="responses"), + "deepseek-v4-pro": ModelConfig(), + }, + ) + flash = create_client(provider, model="deepseek-v4-flash") + assert isinstance(flash, DeepseekResponsesClient) + assert flash.kind == "responses" + pro = create_client(provider, model="deepseek-v4-pro") + assert isinstance(pro, DeepseekOpenAIClient) + + def test_anthropic_client_created() -> None: from plyngent.lmproto.anthropic import AnthropicClient