diff --git a/composer.json b/composer.json index 50415b2..77dd12f 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "^8.2", - "papi-ai/papi-core": "^0.14", + "papi-ai/papi-core": "^0.15", "ext-curl": "*" }, "require-dev": { diff --git a/src/DeepSeekProvider.php b/src/DeepSeekProvider.php index 518de6a..f00c3f0 100644 --- a/src/DeepSeekProvider.php +++ b/src/DeepSeekProvider.php @@ -15,6 +15,7 @@ namespace PapiAI\DeepSeek; use Generator; +use PapiAI\Core\Contracts\NamedToolSelectableInterface; use PapiAI\Core\Contracts\ProviderInterface; use PapiAI\Core\Exception\AuthenticationException; use PapiAI\Core\Exception\ProviderException; @@ -43,13 +44,21 @@ * * @see https://api-docs.deepseek.com/ * - * @psalm-import-type ChatOptions from ProviderInterface + * @psalm-import-type ChatOptions from ProviderInterface * + * The neutral `effort` option is accepted and ignored here. DeepSeek does expose reasoning control, as a nested thinking object rather than a flat level, but papi does not map it yet, so the option is accepted and ignored for now. Ignoring it + * degrades nothing the caller was promised, which is why it is silent where an unhonourable + * `toolChoice` throws. */ -class DeepSeekProvider implements ProviderInterface +class DeepSeekProvider implements ProviderInterface, NamedToolSelectableInterface { private const API_URL = 'https://api.deepseek.com/chat/completions'; + public const MODEL_DEEPSEEK_V4_FLASH = 'deepseek-v4-flash'; + public const MODEL_DEEPSEEK_V4_PRO = 'deepseek-v4-pro'; + + /** @deprecated Discontinued 24 July 2026; requests fail. Use MODEL_DEEPSEEK_V4_FLASH. */ public const MODEL_DEEPSEEK_CHAT = 'deepseek-chat'; + /** @deprecated Discontinued 24 July 2026; requests fail. Use MODEL_DEEPSEEK_V4_FLASH. */ public const MODEL_DEEPSEEK_REASONER = 'deepseek-reasoner'; /** @@ -61,7 +70,7 @@ class DeepSeekProvider implements ProviderInterface */ public function __construct( private readonly string $apiKey, - private readonly string $defaultModel = self::MODEL_DEEPSEEK_CHAT, + private readonly string $defaultModel = self::MODEL_DEEPSEEK_V4_FLASH, private readonly int $defaultMaxTokens = 4096, ) { } diff --git a/tests/Unit/DeepSeekProviderTest.php b/tests/Unit/DeepSeekProviderTest.php index de97e3a..1069b35 100644 --- a/tests/Unit/DeepSeekProviderTest.php +++ b/tests/Unit/DeepSeekProviderTest.php @@ -138,7 +138,7 @@ public function callThrowForStatusCode(int $httpCode, ?array $data): never $this->provider->chat([Message::user('Hello')]); - expect($this->provider->lastPayload['model'])->toBe('deepseek-chat'); + expect($this->provider->lastPayload['model'])->toBe('deepseek-v4-flash'); }); it('overrides model and options from parameters', function () { diff --git a/tests/Unit/DeepSeekToolChoiceTest.php b/tests/Unit/DeepSeekToolChoiceTest.php index 713f173..bf15154 100644 --- a/tests/Unit/DeepSeekToolChoiceTest.php +++ b/tests/Unit/DeepSeekToolChoiceTest.php @@ -12,6 +12,8 @@ declare(strict_types=1); +use PapiAI\Core\Contracts\NamedToolSelectableInterface; +use PapiAI\Core\Contracts\ToolSelectableInterface; use PapiAI\Core\Message; use PapiAI\DeepSeek\DeepSeekProvider; @@ -67,3 +69,10 @@ protected function request(array $payload): array expect($this->provider->lastPayload)->toBe([]); }); }); + +describe('DeepSeekProvider tool-selection capability', function () { + it('declares what it can force, so callers can ask instead of catching', function () { + expect(is_subclass_of(DeepSeekProvider::class, NamedToolSelectableInterface::class))->toBeTrue(); + expect(is_subclass_of(DeepSeekProvider::class, ToolSelectableInterface::class))->toBeTrue(); + }); +});