diff --git a/src/platform/CHANGELOG.md b/src/platform/CHANGELOG.md index 34eb978e..c2b5a2cc 100644 --- a/src/platform/CHANGELOG.md +++ b/src/platform/CHANGELOG.md @@ -58,5 +58,6 @@ CHANGELOG * Add response promises for async operations * Add InMemoryPlatform and InMemoryRawResult for testing Platform without external Providers calls * Add tool calling support for Ollama platform + * Allow beta feature flags to be passed into Anthropic model options diff --git a/src/platform/doc/index.rst b/src/platform/doc/index.rst index 42670e0e..8cb010f3 100644 --- a/src/platform/doc/index.rst +++ b/src/platform/doc/index.rst @@ -65,7 +65,7 @@ start for vendor-specific models and their capabilities, see ``Symfony\AI\Platfo supports a specific feature, like ``Capability::INPUT_AUDIO`` or ``Capability::OUTPUT_IMAGE``. **Options** are additional parameters that can be passed to the model, like ``temperature`` or ``max_tokens``, and are -usually defined by the specific models and their documentation. +usually defined by the specific models and their documentation. Flags for beta features are also passed in here, handled, and then removed before being sent to the provider (beta feature flags are currently only supported for the `Anthropic` platform). A more robust approach for handling provider feature flags may be implemented in the future. **Supported Models & Platforms** diff --git a/src/platform/src/Bridge/Anthropic/ModelClient.php b/src/platform/src/Bridge/Anthropic/ModelClient.php index 978887b3..28843640 100644 --- a/src/platform/src/Bridge/Anthropic/ModelClient.php +++ b/src/platform/src/Bridge/Anthropic/ModelClient.php @@ -39,15 +39,22 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawHttpResult { + $headers = [ + 'x-api-key' => $this->apiKey, + 'anthropic-version' => $this->version, + ]; + if (isset($options['tools'])) { $options['tool_choice'] = ['type' => 'auto']; } + if (isset($options['beta_features']) && !empty($options['beta_features'])) { + $headers['anthropic-beta'] = implode(',', $options['beta_features']); + unset($options['beta_features']); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [ - 'headers' => [ - 'x-api-key' => $this->apiKey, - 'anthropic-version' => $this->version, - ], + 'headers' => $headers, 'json' => array_merge($options, $payload), ])); } diff --git a/src/platform/tests/Bridge/Anthropic/ModelClientTest.php b/src/platform/tests/Bridge/Anthropic/ModelClientTest.php new file mode 100644 index 00000000..62bfbde0 --- /dev/null +++ b/src/platform/tests/Bridge/Anthropic/ModelClientTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Anthropic\Tests; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Anthropic\Claude; +use Symfony\AI\Platform\Bridge\Anthropic\ModelClient; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; + +#[CoversClass(ModelClient::class)] +class ModelClientTest extends TestCase +{ + private MockHttpClient $httpClient; + private ModelClient $modelClient; + private Claude $model; + + protected function setUp(): void + { + $this->model = new Claude(); + } + + private function parseHeaders(array $headers): array + { + $parsed = []; + foreach ($headers as $header) { + if (strpos($header, ':') !== false) { + [$key, $value] = explode(':', $header, 2); + $parsed[trim($key)] = trim($value); + } + } + return $parsed; + } + + public function testAnthropicBetaHeaderIsSetWithSingleBetaFeature(): void + { + $this->httpClient = new MockHttpClient(function ($method, $url, $options) { + $this->assertEquals('POST', $method); + $this->assertEquals('https://api.anthropic.com/v1/messages', $url); + + $headers = $this->parseHeaders($options['headers']); + + $this->assertArrayHasKey('anthropic-beta', $headers); + $this->assertEquals('feature-1', $headers['anthropic-beta']); + + return new MockResponse('{"success": true}'); + }); + + $this->modelClient = new ModelClient($this->httpClient, 'test-api-key'); + + $options = ['beta_features' => ['feature-1']]; + $this->modelClient->request($this->model, ['message' => 'test'], $options); + } + + public function testAnthropicBetaHeaderIsSetWithMultipleBetaFeatures(): void + { + $this->httpClient = new MockHttpClient(function ($method, $url, $options) { + $headers = $this->parseHeaders($options['headers']); + + $this->assertArrayHasKey('anthropic-beta', $headers); + $this->assertEquals('feature-1,feature-2,feature-3', $headers['anthropic-beta']); + + return new MockResponse('{"success": true}'); + }); + + $this->modelClient = new ModelClient($this->httpClient, 'test-api-key', '2023-06-01'); + + $options = ['beta_features' => ['feature-1', 'feature-2', 'feature-3']]; + $this->modelClient->request($this->model, ['message' => 'test'], $options); + } + + public function testAnthropicBetaHeaderIsNotSetWhenBetaFeaturesIsEmpty(): void + { + $this->httpClient = new MockHttpClient(function ($method, $url, $options) { + $headers = $this->parseHeaders($options['headers']); + + $this->assertArrayNotHasKey('anthropic-beta', $headers); + + return new MockResponse('{"success": true}'); + }); + + $this->modelClient = new ModelClient($this->httpClient, 'test-api-key', '2023-06-01'); + + $options = ['beta_features' => []]; + $this->modelClient->request($this->model, ['message' => 'test'], $options); + } + + public function testAnthropicBetaHeaderIsNotSetWhenBetaFeaturesIsNotProvided(): void + { + $this->httpClient = new MockHttpClient(function ($method, $url, $options) { + $headers = $this->parseHeaders($options['headers']); + + $this->assertArrayNotHasKey('anthropic-beta', $headers); + + return new MockResponse('{"success": true}'); + }); + + $this->modelClient = new ModelClient($this->httpClient, 'test-api-key', '2023-06-01'); + + $options = ['some_other_option' => 'value']; + $this->modelClient->request($this->model, ['message' => 'test'], $options); + } +}