diff --git a/composer.json b/composer.json index 8301f24..b2bc698 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/CohereProvider.php b/src/CohereProvider.php index 5db3f42..2687709 100644 --- a/src/CohereProvider.php +++ b/src/CohereProvider.php @@ -17,6 +17,7 @@ use Generator; use PapiAI\Core\Contracts\EmbeddingProviderInterface; use PapiAI\Core\Contracts\ProviderInterface; +use PapiAI\Core\Contracts\ToolSelectableInterface; use PapiAI\Core\EmbeddingResponse; use PapiAI\Core\Exception\AuthenticationException; use PapiAI\Core\Exception\ProviderException; @@ -35,15 +36,26 @@ * Supports chat completions, streaming, tool calling, and embeddings. * Authentication via Bearer token. All HTTP via ext-curl. * - * @see https://docs.cohere.com/reference/chat + * @see https://docs.cohere.com/reference/chat * + * The neutral `effort` option is accepted and ignored here. Cohere exposes no reasoning-effort parameter on the v2 chat API. Ignoring it + * degrades nothing the caller was promised, which is why it is silent where an unhonourable + * `toolChoice` throws. */ -class CohereProvider implements ProviderInterface, EmbeddingProviderInterface +class CohereProvider implements ProviderInterface, EmbeddingProviderInterface, ToolSelectableInterface { private const CHAT_API_URL = 'https://api.cohere.com/v2/chat'; private const EMBED_API_URL = 'https://api.cohere.com/v1/embed'; + public const MODEL_COMMAND_A_PLUS = 'command-a-plus-05-2026'; + public const MODEL_COMMAND_A = 'command-a-03-2025'; + public const MODEL_COMMAND_A_REASONING = 'command-a-reasoning-08-2025'; + public const MODEL_COMMAND_R7B = 'command-r7b-12-2024'; + + /** @deprecated Deprecated 15 September 2025, and predates command-r7b so it rejects tool_choice. */ public const MODEL_COMMAND_R_PLUS = 'command-r-plus'; + /** @deprecated Deprecated 15 September 2025, and predates command-r7b so it rejects tool_choice. */ public const MODEL_COMMAND_R = 'command-r'; + /** @deprecated Deprecated 15 September 2025. */ public const MODEL_COMMAND = 'command'; public const MODEL_EMBED_ENGLISH = 'embed-english-v3.0'; @@ -55,7 +67,7 @@ class CohereProvider implements ProviderInterface, EmbeddingProviderInterface */ public function __construct( private readonly string $apiKey, - private readonly string $defaultModel = self::MODEL_COMMAND_R_PLUS, + private readonly string $defaultModel = self::MODEL_COMMAND_A_PLUS, ) { } diff --git a/tests/Unit/CohereProviderTest.php b/tests/Unit/CohereProviderTest.php index 9495115..218192e 100644 --- a/tests/Unit/CohereProviderTest.php +++ b/tests/Unit/CohereProviderTest.php @@ -162,7 +162,7 @@ public function callHandleError(int $httpCode, ?array $data): void $this->provider->chat([Message::user('Hello')]); - expect($this->provider->lastPayload['model'])->toBe('command-r-plus'); + expect($this->provider->lastPayload['model'])->toBe('command-a-plus-05-2026'); }); it('overrides model and options from parameters', function () { diff --git a/tests/Unit/CohereToolChoiceTest.php b/tests/Unit/CohereToolChoiceTest.php index 41e1cc9..e87381f 100644 --- a/tests/Unit/CohereToolChoiceTest.php +++ b/tests/Unit/CohereToolChoiceTest.php @@ -13,6 +13,8 @@ declare(strict_types=1); use PapiAI\Cohere\CohereProvider; +use PapiAI\Core\Contracts\NamedToolSelectableInterface; +use PapiAI\Core\Contracts\ToolSelectableInterface; use PapiAI\Core\Exception\ProviderException; use PapiAI\Core\Message; @@ -94,3 +96,11 @@ protected function request(array $payload): array ->toThrow(InvalidArgumentException::class); }); }); + +describe('CohereProvider tool-selection capability', function () { + it('declares what it can force, so callers can ask instead of catching', function () { + // Cohere can force "required" or "none", but its API cannot name a tool. + expect(is_subclass_of(CohereProvider::class, ToolSelectableInterface::class))->toBeTrue(); + expect(is_subclass_of(CohereProvider::class, NamedToolSelectableInterface::class))->toBeFalse(); + }); +});