From b7feaa04fa8a263af5a878b460cef529b703c61e Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 16:28:08 +0100 Subject: [PATCH 01/15] feat(platform): allow configure bedrock platform with support of multiple instances --- src/ai-bundle/config/options.php | 9 +++++++ src/ai-bundle/config/services.php | 2 ++ src/ai-bundle/src/AiBundle.php | 26 +++++++++++++++++++ .../src/Bridge/Bedrock/PlatformFactory.php | 2 +- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 1a3396e44..c2af7a5b8 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -62,6 +62,15 @@ ->end() ->end() ->end() + ->arrayNode('bedrock') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->stringNode('bedrock_runtime_client')->isRequired()->end() + ->stringNode('model_catalog')->defaultNull()->end() + ->end() + ->end() + ->end() ->arrayNode('cache') ->useAttributeAsKey('name') ->arrayPrototype() diff --git a/src/ai-bundle/config/services.php b/src/ai-bundle/config/services.php index 6980570d1..4ea8963fd 100644 --- a/src/ai-bundle/config/services.php +++ b/src/ai-bundle/config/services.php @@ -29,6 +29,7 @@ use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract; use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog; use Symfony\AI\Platform\Bridge\Azure\OpenAi\ModelCatalog as AzureOpenAiModelCatalog; +use Symfony\AI\Platform\Bridge\Bedrock\ModelCatalog as BedrockModelCatalog; use Symfony\AI\Platform\Bridge\Cartesia\ModelCatalog as CartesiaModelCatalog; use Symfony\AI\Platform\Bridge\Cerebras\ModelCatalog as CerebrasModelCatalog; use Symfony\AI\Platform\Bridge\Decart\ModelCatalog as DecartModelCatalog; @@ -96,6 +97,7 @@ ->set('ai.platform.model_catalog.albert', AlbertModelCatalog::class) ->set('ai.platform.model_catalog.anthropic', AnthropicModelCatalog::class) ->set('ai.platform.model_catalog.azure.openai', AzureOpenAiModelCatalog::class) + ->set('ai.platform.model_catalog.bedrock', BedrockModelCatalog::class) ->set('ai.platform.model_catalog.cartesia', CartesiaModelCatalog::class) ->set('ai.platform.model_catalog.cerebras', CerebrasModelCatalog::class) ->set('ai.platform.model_catalog.decart', DecartModelCatalog::class) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index af38f73cd..dda7f3e22 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -53,6 +53,7 @@ use Symfony\AI\Platform\Bridge\Albert\PlatformFactory as AlbertPlatformFactory; use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory; use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory; +use Symfony\AI\Platform\Bridge\Bedrock\PlatformFactory as BedrockFactory; use Symfony\AI\Platform\Bridge\Cartesia\PlatformFactory as CartesiaPlatformFactory; use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory; use Symfony\AI\Platform\Bridge\Decart\PlatformFactory as DecartPlatformFactory; @@ -408,6 +409,31 @@ private function processPlatformConfig(string $type, array $platform, ContainerB return; } + if ('bedrock' === $type) { + if (!ContainerBuilder::willBeAvailable('symfony/bedrock-platform', BedrockFactory::class, ['symfony/ai-bundle'])) { + throw new RuntimeException('Bedrock platform configuration requires "symfony/bedrock-platform" package. Try running "composer require symfony/bedrock-platform".'); + } + + foreach ($platform as $name => $config) { + $platformId = 'ai.platform.bedrock.'.$name; + $definition = (new Definition(Platform::class)) + ->setFactory(BedrockFactory::class.'::create') + ->setLazy(true) + ->addTag('proxy', ['interface' => PlatformInterface::class]) + ->setArguments([ + new Reference($config['bedrock_runtime_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference('ai.platform.model_catalog.bedrock'), + $config['model_catalog'] ? new Reference($config['model_catalog']) : null, + new Reference('event_dispatcher'), + ]) + ->addTag('ai.platform', ['name' => 'bedrock.'.$name]); + + $container->setDefinition($platformId, $definition); + } + + return; + } + if ('cache' === $type) { foreach ($platform as $name => $cachedPlatformConfig) { $definition = (new Definition(CachedPlatform::class)) diff --git a/src/platform/src/Bridge/Bedrock/PlatformFactory.php b/src/platform/src/Bridge/Bedrock/PlatformFactory.php index 2d7632fe0..a28d874e0 100644 --- a/src/platform/src/Bridge/Bedrock/PlatformFactory.php +++ b/src/platform/src/Bridge/Bedrock/PlatformFactory.php @@ -33,7 +33,7 @@ final class PlatformFactory { public static function create( - BedrockRuntimeClient $bedrockRuntimeClient = new BedrockRuntimeClient(), + BedrockRuntimeClient $bedrockRuntimeClient, ModelCatalogInterface $modelCatalog = new ModelCatalog(), ?Contract $contract = null, ?EventDispatcherInterface $eventDispatcher = null, From 29e001f1562de990ea61988463ca7034c80d8404 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 16:29:04 +0100 Subject: [PATCH 02/15] fix(nova): avoid populate model name to message payload --- src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php b/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php index a1990da62..31329a0c4 100644 --- a/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php +++ b/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php @@ -34,6 +34,8 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawBedrockResult { + unset($payload['model']); + $modelOptions = []; if (isset($options['tools'])) { $modelOptions['toolConfig']['tools'] = $options['tools']; From d880a217c858c9774e3aec485bfa6a36b4133905 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 16:29:42 +0100 Subject: [PATCH 03/15] chore(claude): removed unnesessary convert method (as its converted through response conversion) --- .../Bedrock/Anthropic/ClaudeModelClient.php | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php index 1d47b7690..c780bba92 100644 --- a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php +++ b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php @@ -60,31 +60,6 @@ public function request(Model $model, array|string $payload, array $options = [] return new RawBedrockResult($this->bedrockRuntimeClient->invokeModel(new InvokeModelRequest($request))); } - public function convert(InvokeModelResponse $bedrockResponse): ToolCallResult|TextResult - { - $data = json_decode($bedrockResponse->getBody(), true, 512, \JSON_THROW_ON_ERROR); - - if (!isset($data['content']) || [] === $data['content']) { - throw new RuntimeException('Response does not contain any content.'); - } - - if (!isset($data['content'][0]['text']) && !isset($data['content'][0]['type'])) { - throw new RuntimeException('Response content does not contain any text or type.'); - } - - $toolCalls = []; - foreach ($data['content'] as $content) { - if ('tool_use' === $content['type']) { - $toolCalls[] = new ToolCall($content['id'], $content['name'], $content['input']); - } - } - if ([] !== $toolCalls) { - return new ToolCallResult(...$toolCalls); - } - - return new TextResult($data['content'][0]['text']); - } - private function getModelId(Model $model): string { $configuredRegion = $this->bedrockRuntimeClient->getConfiguration()->get('region'); From d81102c921401429987575a8f29334a9d9762f99 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 16:31:59 +0100 Subject: [PATCH 04/15] chore(bedrock): throw exception if client is missing --- src/ai-bundle/src/AiBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index dda7f3e22..79cea3385 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -396,7 +396,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB $config['deployment'], $config['api_version'], $config['api_key'], - new Reference($config['http_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference($config['http_client']), new Reference('ai.platform.model_catalog.azure.openai'), new Reference('ai.platform.contract.openai'), new Reference('event_dispatcher'), From 517693ba52b314d04d5677a0f31be13123e406e7 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 17:35:39 +0100 Subject: [PATCH 05/15] chore(bedrock): ensure bedrock platform can be initiated via bundle config --- src/ai-bundle/config/options.php | 5 ++++- src/ai-bundle/src/AiBundle.php | 4 ++-- src/ai-bundle/tests/DependencyInjection/AiBundleTest.php | 7 +++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index c2af7a5b8..d1ac42b13 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -66,7 +66,10 @@ ->useAttributeAsKey('name') ->arrayPrototype() ->children() - ->stringNode('bedrock_runtime_client')->isRequired()->end() + ->stringNode('bedrock_runtime_client') + ->isRequired() + ->info('Service ID of the Bedrock runtime client to use') + ->end() ->stringNode('model_catalog')->defaultNull()->end() ->end() ->end() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 79cea3385..784c17774 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -396,7 +396,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB $config['deployment'], $config['api_version'], $config['api_key'], - new Reference($config['http_client']), + new Reference($config['http_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE), new Reference('ai.platform.model_catalog.azure.openai'), new Reference('ai.platform.contract.openai'), new Reference('event_dispatcher'), @@ -421,7 +421,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB ->setLazy(true) ->addTag('proxy', ['interface' => PlatformInterface::class]) ->setArguments([ - new Reference($config['bedrock_runtime_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference($config['bedrock_runtime_client']), new Reference('ai.platform.model_catalog.bedrock'), $config['model_catalog'] ? new Reference($config['model_catalog']) : null, new Reference('event_dispatcher'), diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index ed2157c22..11aa0f5ae 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -11,6 +11,7 @@ namespace Symfony\AI\AiBundle\Tests\DependencyInjection; +use AsyncAws\BedrockRuntime\BedrockRuntimeClient; use Codewithkyrian\ChromaDB\Client; use MongoDB\Client as MongoDbClient; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; @@ -7013,6 +7014,7 @@ private function buildContainer(array $configuration): ContainerBuilder $container->setParameter('kernel.environment', 'dev'); $container->setParameter('kernel.build_dir', 'public'); $container->setDefinition(ClockInterface::class, new Definition(MonotonicClock::class)); + $container->setDefinition('async_aws.client.bedrock', new Definition(BedrockRuntimeClient::class)); $extension = (new AiBundle())->getContainerExtension(); $extension->load($configuration, $container); @@ -7049,6 +7051,11 @@ private function getFullConfig(): array 'api_version' => '2024-02-15-preview', ], ], + 'bedrock' => [ + 'default' => [ + 'bedrock_runtime_client' => 'async_aws.client.bedrock', + ], + ], 'cache' => [ 'azure' => [ 'platform' => 'ai.platform.azure.my_azure_instance', From b892e5e16a948b10a07658e9e81346013963ead8 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 18:45:02 +0100 Subject: [PATCH 06/15] chore(bedrock): cover model clients with tests --- .../Tests/Anthropic/ClaudeModelClientTest.php | 132 ++++++++++++ .../Tests/Nova/NovaModelClientTest.php | 190 ++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php create mode 100644 src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php diff --git a/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php new file mode 100644 index 000000000..8d5e0926a --- /dev/null +++ b/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Bedrock\Tests\Anthropic; + +use AsyncAws\BedrockRuntime\BedrockRuntimeClient; +use AsyncAws\BedrockRuntime\Input\InvokeModelRequest; +use AsyncAws\BedrockRuntime\Result\InvokeModelResponse; +use AsyncAws\Core\Configuration; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Anthropic\Claude; +use Symfony\AI\Platform\Bridge\Bedrock\Anthropic\ClaudeModelClient; +use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; + +final class ClaudeModelClientTest extends TestCase +{ + private const VERSION = '2023-05-31'; + + private MockObject&BedrockRuntimeClient $bedrockClient; + private ClaudeModelClient $modelClient; + private Claude $model; + + protected function setUp(): void + { + $this->model = new Claude('claude-sonnet-4-5-20250929'); + $this->bedrockClient = $this->getMockBuilder(BedrockRuntimeClient::class) + ->setConstructorArgs([ + Configuration::create([Configuration::OPTION_REGION => Configuration::DEFAULT_REGION]), + ]) + ->onlyMethods(['invokeModel']) + ->getMock(); + } + + public function testPassesModelId() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('us.anthropic.claude-sonnet-4-5-20250929-v1:0', $arg->getModelId()); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new ClaudeModelClient($this->bedrockClient, self::VERSION); + + $response = $this->modelClient->request($this->model, ['message' => 'test']); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testUnsetsModelName() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertArrayNotHasKey('model', $body); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new ClaudeModelClient($this->bedrockClient, self::VERSION); + + $response = $this->modelClient->request($this->model, ['message' => 'test', 'model' => 'claude']); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testSetsAnthropicVersion() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertEquals('bedrock-'.self::VERSION, $body['anthropic_version']); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new ClaudeModelClient($this->bedrockClient, self::VERSION); + + $response = $this->modelClient->request($this->model, ['message' => 'test']); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testSetsToolOptionsIfToolsEnabled() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertEquals(['type' => 'auto'], $body['tool_choice']); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new ClaudeModelClient($this->bedrockClient, self::VERSION); + + $options = [ + 'tools' => ['Tool'] + ]; + + $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } +} diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php new file mode 100644 index 000000000..423b4b058 --- /dev/null +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Bedrock\Tests\Nova; + +use AsyncAws\BedrockRuntime\BedrockRuntimeClient; +use AsyncAws\BedrockRuntime\Input\InvokeModelRequest; +use AsyncAws\BedrockRuntime\Result\InvokeModelResponse; +use AsyncAws\Core\Configuration; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Bedrock\Nova\Nova; +use Symfony\AI\Platform\Bridge\Bedrock\Nova\NovaModelClient; +use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; + +final class NovaModelClientTest extends TestCase +{ + private MockObject&BedrockRuntimeClient $bedrockClient; + private NovaModelClient $modelClient; + private Nova $model; + + protected function setUp(): void + { + $this->model = new Nova('nova-pro'); + $this->bedrockClient = $this->getMockBuilder(BedrockRuntimeClient::class) + ->setConstructorArgs([ + Configuration::create([Configuration::OPTION_REGION => Configuration::DEFAULT_REGION]), + ]) + ->onlyMethods(['invokeModel']) + ->getMock(); + } + + public function testPassesModelId() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('us.amazon.nova-pro-v1:0', $arg->getModelId()); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new NovaModelClient($this->bedrockClient); + + $response = $this->modelClient->request($this->model, ['message' => 'test']); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testUnsetsModelName() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertArrayNotHasKey('model', $body); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new NovaModelClient($this->bedrockClient); + + $response = $this->modelClient->request($this->model, ['message' => 'test', 'model' => 'nova-pro']); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testSetsToolOptionsIfToolsEnabled() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertEquals(['tools' => ['Tool']], $body['toolConfig']); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new NovaModelClient($this->bedrockClient); + + $options = [ + 'tools' => ['Tool'] + ]; + + $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testPassesTemperature() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertArrayHasKey('inferenceConfig', $body); + $this->assertEquals(['temperature' => 0.35], $body['inferenceConfig']); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new NovaModelClient($this->bedrockClient); + + $options = [ + 'temperature' => 0.35 + ]; + + $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testPassesMaxTokens() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertArrayHasKey('inferenceConfig', $body); + $this->assertEquals(['maxTokens' => 1000], $body['inferenceConfig']); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new NovaModelClient($this->bedrockClient); + + $options = [ + 'max_tokens' => 1000 + ]; + + $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } + + public function testPassesBothTemperatureAndMaxTokens() + { + $this->bedrockClient->expects(self::once()) + ->method('invokeModel') + ->with($this->callback(function ($arg) { + $this->assertInstanceOf(InvokeModelRequest::class, $arg); + $this->assertEquals('application/json', $arg->getContentType()); + $this->assertTrue(json_validate($arg->getBody())); + + $body = json_decode($arg->getBody(), true); + $this->assertArrayHasKey('inferenceConfig', $body); + $this->assertEquals(['temperature' => 0.35, 'maxTokens' => 1000], $body['inferenceConfig']); + + return true; + })) + ->willReturn($this->createMock(InvokeModelResponse::class)); + + $this->modelClient = new NovaModelClient($this->bedrockClient); + + $options = [ + 'max_tokens' => 1000, + 'temperature' => 0.35 + ]; + + $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); + $this->assertInstanceOf(RawBedrockResult::class, $response); + } +} From b892aefc83c80131ce722fdbc22e69999bee9cc2 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 20:22:10 +0100 Subject: [PATCH 07/15] chore(bedrock): allow pass null as bedrock client --- docs/bundles/ai-bundle.rst | 9 +++++++++ src/ai-bundle/config/options.php | 2 +- src/ai-bundle/src/AiBundle.php | 12 ++++++------ .../tests/DependencyInjection/AiBundleTest.php | 7 ++++--- src/platform/src/Bridge/Bedrock/PlatformFactory.php | 6 +++++- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/bundles/ai-bundle.rst b/docs/bundles/ai-bundle.rst index ecbe540bb..da4837988 100644 --- a/docs/bundles/ai-bundle.rst +++ b/docs/bundles/ai-bundle.rst @@ -51,6 +51,11 @@ Advanced Example with Multiple Agents deployment: '%env(AZURE_OPENAI_GPT)%' api_key: '%env(AZURE_OPENAI_KEY)%' api_version: '%env(AZURE_GPT_VERSION)%' + bedrock: + # multiple instances possible - for example region depending + default: ~ + eu: + bedrock_runtime_client: 'async_aws.client.bedrock_runtime_eu' eleven_labs: host: '%env(ELEVEN_LABS_HOST)%' api_key: '%env(ELEVEN_LABS_API_KEY)%' @@ -100,6 +105,10 @@ Advanced Example with Multiple Agents platform: 'ai.platform.eleven_labs' model: 'text-to-speech' tools: false + nova: + platform: 'ai.platform.bedrock_default + model: 'nova-pro' + tools: false store: chromadb: # multiple collections possible per type diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index d1ac42b13..a87be44f1 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -67,7 +67,7 @@ ->arrayPrototype() ->children() ->stringNode('bedrock_runtime_client') - ->isRequired() + ->defaultNull() ->info('Service ID of the Bedrock runtime client to use') ->end() ->stringNode('model_catalog')->defaultNull()->end() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 784c17774..a8aeffb17 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -177,7 +177,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->setDecoratedService($platform) ->setArguments([new Reference('.inner')]) ->addTag('ai.traceable_platform'); - $suffix = u($platform)->afterLast('.')->toString(); + $suffix = u($platform)->replace('ai.platform.', '')->toString(); $builder->setDefinition('ai.traceable_platform.'.$suffix, $traceablePlatformDefinition); } } @@ -415,18 +415,18 @@ private function processPlatformConfig(string $type, array $platform, ContainerB } foreach ($platform as $name => $config) { - $platformId = 'ai.platform.bedrock.'.$name; + $platformId = 'ai.platform.bedrock_'.$name; $definition = (new Definition(Platform::class)) ->setFactory(BedrockFactory::class.'::create') ->setLazy(true) ->addTag('proxy', ['interface' => PlatformInterface::class]) ->setArguments([ - new Reference($config['bedrock_runtime_client']), - new Reference('ai.platform.model_catalog.bedrock'), - $config['model_catalog'] ? new Reference($config['model_catalog']) : null, + $config['bedrock_runtime_client'] ? new Reference($config['bedrock_runtime_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE) : null, + $config['model_catalog'] ? new Reference($config['model_catalog']) : new Reference('ai.platform.model_catalog.bedrock'), + null, new Reference('event_dispatcher'), ]) - ->addTag('ai.platform', ['name' => 'bedrock.'.$name]); + ->addTag('ai.platform', ['name' => 'bedrock_'.$name]); $container->setDefinition($platformId, $definition); } diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 11aa0f5ae..79afaf4e0 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -7014,7 +7014,7 @@ private function buildContainer(array $configuration): ContainerBuilder $container->setParameter('kernel.environment', 'dev'); $container->setParameter('kernel.build_dir', 'public'); $container->setDefinition(ClockInterface::class, new Definition(MonotonicClock::class)); - $container->setDefinition('async_aws.client.bedrock', new Definition(BedrockRuntimeClient::class)); + $container->setDefinition('async_aws.client.bedrock_us', new Definition(BedrockRuntimeClient::class)); $extension = (new AiBundle())->getContainerExtension(); $extension->load($configuration, $container); @@ -7052,8 +7052,9 @@ private function getFullConfig(): array ], ], 'bedrock' => [ - 'default' => [ - 'bedrock_runtime_client' => 'async_aws.client.bedrock', + 'default' => [], + 'us' => [ + 'bedrock_runtime_client' => 'async_aws.client.bedrock_us', ], ], 'cache' => [ diff --git a/src/platform/src/Bridge/Bedrock/PlatformFactory.php b/src/platform/src/Bridge/Bedrock/PlatformFactory.php index a28d874e0..999bb5320 100644 --- a/src/platform/src/Bridge/Bedrock/PlatformFactory.php +++ b/src/platform/src/Bridge/Bedrock/PlatformFactory.php @@ -33,7 +33,7 @@ final class PlatformFactory { public static function create( - BedrockRuntimeClient $bedrockRuntimeClient, + ?BedrockRuntimeClient $bedrockRuntimeClient = null, ModelCatalogInterface $modelCatalog = new ModelCatalog(), ?Contract $contract = null, ?EventDispatcherInterface $eventDispatcher = null, @@ -42,6 +42,10 @@ public static function create( throw new RuntimeException('For using the Bedrock platform, the async-aws/bedrock-runtime package is required. Try running "composer require async-aws/bedrock-runtime".'); } + if (!$bedrockRuntimeClient) { + $bedrockRuntimeClient = new BedrockRuntimeClient(); + } + return new Platform( [ new ClaudeModelClient($bedrockRuntimeClient), From fcd9546d45c49a2ab530e4c9992f681c7c04eaf5 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 20:27:18 +0100 Subject: [PATCH 08/15] chore(bedrock): rollback unnecessary change --- src/ai-bundle/src/AiBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index a8aeffb17..a80708513 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -177,7 +177,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->setDecoratedService($platform) ->setArguments([new Reference('.inner')]) ->addTag('ai.traceable_platform'); - $suffix = u($platform)->replace('ai.platform.', '')->toString(); + $suffix = u($platform)->afterLast('.')->toString(); $builder->setDefinition('ai.traceable_platform.'.$suffix, $traceablePlatformDefinition); } } From ca327504e706938eea15c6c596ac77ada6003339 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 20:39:26 +0100 Subject: [PATCH 09/15] chore(cs): fixing cs --- .../Bedrock/Anthropic/ClaudeModelClient.php | 5 ----- .../Tests/Anthropic/ClaudeModelClientTest.php | 10 +++++----- .../Tests/Nova/NovaModelClientTest.php | 20 +++++++++---------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php index c780bba92..edaa604d7 100644 --- a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php +++ b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php @@ -13,15 +13,10 @@ use AsyncAws\BedrockRuntime\BedrockRuntimeClient; use AsyncAws\BedrockRuntime\Input\InvokeModelRequest; -use AsyncAws\BedrockRuntime\Result\InvokeModelResponse; use Symfony\AI\Platform\Bridge\Anthropic\Claude; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; -use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\AI\Platform\Result\ToolCall; -use Symfony\AI\Platform\Result\ToolCallResult; /** * @author Björn Altmann diff --git a/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php index 8d5e0926a..00869a93b 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php @@ -42,7 +42,7 @@ protected function setUp(): void public function testPassesModelId() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -62,7 +62,7 @@ public function testPassesModelId() public function testUnsetsModelName() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -84,7 +84,7 @@ public function testUnsetsModelName() public function testSetsAnthropicVersion() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -106,7 +106,7 @@ public function testSetsAnthropicVersion() public function testSetsToolOptionsIfToolsEnabled() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -123,7 +123,7 @@ public function testSetsToolOptionsIfToolsEnabled() $this->modelClient = new ClaudeModelClient($this->bedrockClient, self::VERSION); $options = [ - 'tools' => ['Tool'] + 'tools' => ['Tool'], ]; $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php index 423b4b058..e456218e8 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php @@ -40,7 +40,7 @@ protected function setUp(): void public function testPassesModelId() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -60,7 +60,7 @@ public function testPassesModelId() public function testUnsetsModelName() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -82,7 +82,7 @@ public function testUnsetsModelName() public function testSetsToolOptionsIfToolsEnabled() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -99,7 +99,7 @@ public function testSetsToolOptionsIfToolsEnabled() $this->modelClient = new NovaModelClient($this->bedrockClient); $options = [ - 'tools' => ['Tool'] + 'tools' => ['Tool'], ]; $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); @@ -108,7 +108,7 @@ public function testSetsToolOptionsIfToolsEnabled() public function testPassesTemperature() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -126,7 +126,7 @@ public function testPassesTemperature() $this->modelClient = new NovaModelClient($this->bedrockClient); $options = [ - 'temperature' => 0.35 + 'temperature' => 0.35, ]; $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); @@ -135,7 +135,7 @@ public function testPassesTemperature() public function testPassesMaxTokens() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -153,7 +153,7 @@ public function testPassesMaxTokens() $this->modelClient = new NovaModelClient($this->bedrockClient); $options = [ - 'max_tokens' => 1000 + 'max_tokens' => 1000, ]; $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); @@ -162,7 +162,7 @@ public function testPassesMaxTokens() public function testPassesBothTemperatureAndMaxTokens() { - $this->bedrockClient->expects(self::once()) + $this->bedrockClient->expects($this->once()) ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); @@ -181,7 +181,7 @@ public function testPassesBothTemperatureAndMaxTokens() $options = [ 'max_tokens' => 1000, - 'temperature' => 0.35 + 'temperature' => 0.35, ]; $response = $this->modelClient->request($this->model, ['message' => 'test'], $options); From e005a9ab52dbea66d4178e283e89dd92e60c52b2 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 20:46:09 +0100 Subject: [PATCH 10/15] fix(demo): fixing typo in package name --- src/ai-bundle/src/AiBundle.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index a80708513..9bf4e90c5 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -410,8 +410,8 @@ private function processPlatformConfig(string $type, array $platform, ContainerB } if ('bedrock' === $type) { - if (!ContainerBuilder::willBeAvailable('symfony/bedrock-platform', BedrockFactory::class, ['symfony/ai-bundle'])) { - throw new RuntimeException('Bedrock platform configuration requires "symfony/bedrock-platform" package. Try running "composer require symfony/bedrock-platform".'); + if (!ContainerBuilder::willBeAvailable('symfony/ai-bedrock-platform', BedrockFactory::class, ['symfony/ai-bundle'])) { + throw new RuntimeException('Bedrock platform configuration requires "symfony/ai-bedrock-platform" package. Try running "composer require symfony/ai-bedrock-platform".'); } foreach ($platform as $name => $config) { From ffa4589891418a017accb4d486de02482f091c9a Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:00:53 +0100 Subject: [PATCH 11/15] chore(cs): strict comparison to null for passed bedrock runtime client --- src/platform/src/Bridge/Bedrock/PlatformFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/src/Bridge/Bedrock/PlatformFactory.php b/src/platform/src/Bridge/Bedrock/PlatformFactory.php index 999bb5320..be2af8485 100644 --- a/src/platform/src/Bridge/Bedrock/PlatformFactory.php +++ b/src/platform/src/Bridge/Bedrock/PlatformFactory.php @@ -42,7 +42,7 @@ public static function create( throw new RuntimeException('For using the Bedrock platform, the async-aws/bedrock-runtime package is required. Try running "composer require async-aws/bedrock-runtime".'); } - if (!$bedrockRuntimeClient) { + if (null === $bedrockRuntimeClient) { $bedrockRuntimeClient = new BedrockRuntimeClient(); } From 378c58f4ee42cc977d66077c0326073aeadfd7d5 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:04:05 +0100 Subject: [PATCH 12/15] chore(cs): assertEquals -> assertSame --- .../Tests/Anthropic/ClaudeModelClientTest.php | 14 ++++++------ .../Bedrock/Tests/Nova/ContractTest.php | 2 +- .../Tests/Nova/NovaModelClientTest.php | 22 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php index 00869a93b..fdf510062 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Anthropic/ClaudeModelClientTest.php @@ -46,8 +46,8 @@ public function testPassesModelId() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('us.anthropic.claude-sonnet-4-5-20250929-v1:0', $arg->getModelId()); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('us.anthropic.claude-sonnet-4-5-20250929-v1:0', $arg->getModelId()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); return true; @@ -66,7 +66,7 @@ public function testUnsetsModelName() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); @@ -88,11 +88,11 @@ public function testSetsAnthropicVersion() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); - $this->assertEquals('bedrock-'.self::VERSION, $body['anthropic_version']); + $this->assertSame('bedrock-'.self::VERSION, $body['anthropic_version']); return true; })) @@ -110,11 +110,11 @@ public function testSetsToolOptionsIfToolsEnabled() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); - $this->assertEquals(['type' => 'auto'], $body['tool_choice']); + $this->assertSame(['type' => 'auto'], $body['tool_choice']); return true; })) diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php index 33a285fe3..1edf80151 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php @@ -37,7 +37,7 @@ public function testConvert(MessageBag $bag, array $expected) new UserMessageNormalizer(), ); - $this->assertEquals($expected, $contract->createRequestPayload(new Nova('nova-pro'), $bag)); + $this->assertSame($expected, $contract->createRequestPayload(new Nova('nova-pro'), $bag)); } /** diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php index e456218e8..0f1195e76 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php @@ -44,8 +44,8 @@ public function testPassesModelId() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('us.amazon.nova-pro-v1:0', $arg->getModelId()); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('us.amazon.nova-pro-v1:0', $arg->getModelId()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); return true; @@ -64,7 +64,7 @@ public function testUnsetsModelName() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); @@ -86,11 +86,11 @@ public function testSetsToolOptionsIfToolsEnabled() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); - $this->assertEquals(['tools' => ['Tool']], $body['toolConfig']); + $this->assertSame(['tools' => ['Tool']], $body['toolConfig']); return true; })) @@ -112,12 +112,12 @@ public function testPassesTemperature() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); $this->assertArrayHasKey('inferenceConfig', $body); - $this->assertEquals(['temperature' => 0.35], $body['inferenceConfig']); + $this->assertSame(['temperature' => 0.35], $body['inferenceConfig']); return true; })) @@ -139,12 +139,12 @@ public function testPassesMaxTokens() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); $this->assertArrayHasKey('inferenceConfig', $body); - $this->assertEquals(['maxTokens' => 1000], $body['inferenceConfig']); + $this->assertSame(['maxTokens' => 1000], $body['inferenceConfig']); return true; })) @@ -166,12 +166,12 @@ public function testPassesBothTemperatureAndMaxTokens() ->method('invokeModel') ->with($this->callback(function ($arg) { $this->assertInstanceOf(InvokeModelRequest::class, $arg); - $this->assertEquals('application/json', $arg->getContentType()); + $this->assertSame('application/json', $arg->getContentType()); $this->assertTrue(json_validate($arg->getBody())); $body = json_decode($arg->getBody(), true); $this->assertArrayHasKey('inferenceConfig', $body); - $this->assertEquals(['temperature' => 0.35, 'maxTokens' => 1000], $body['inferenceConfig']); + $this->assertSame(['temperature' => 0.35, 'maxTokens' => 1000], $body['inferenceConfig']); return true; })) From 8f14ba0270c37b0883e4c6609fabd758d6c033e5 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:04:16 +0100 Subject: [PATCH 13/15] chore(cs): missing quote --- docs/bundles/ai-bundle.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bundles/ai-bundle.rst b/docs/bundles/ai-bundle.rst index da4837988..388ad3b05 100644 --- a/docs/bundles/ai-bundle.rst +++ b/docs/bundles/ai-bundle.rst @@ -106,7 +106,7 @@ Advanced Example with Multiple Agents model: 'text-to-speech' tools: false nova: - platform: 'ai.platform.bedrock_default + platform: 'ai.platform.bedrock_default' model: 'nova-pro' tools: false store: From 98e7ab2e0d5e2301fac430f7a03449d377234bbd Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:10:09 +0100 Subject: [PATCH 14/15] fix(bundle): check for bedrock platform package only in case at least one configured --- src/ai-bundle/src/AiBundle.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 9bf4e90c5..2aae5ad58 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -410,11 +410,11 @@ private function processPlatformConfig(string $type, array $platform, ContainerB } if ('bedrock' === $type) { - if (!ContainerBuilder::willBeAvailable('symfony/ai-bedrock-platform', BedrockFactory::class, ['symfony/ai-bundle'])) { - throw new RuntimeException('Bedrock platform configuration requires "symfony/ai-bedrock-platform" package. Try running "composer require symfony/ai-bedrock-platform".'); - } - foreach ($platform as $name => $config) { + if (!ContainerBuilder::willBeAvailable('symfony/ai-bedrock-platform', BedrockFactory::class, ['symfony/ai-bundle'])) { + throw new RuntimeException('Bedrock platform configuration requires "symfony/ai-bedrock-platform" package. Try running "composer require symfony/ai-bedrock-platform".'); + } + $platformId = 'ai.platform.bedrock_'.$name; $definition = (new Definition(Platform::class)) ->setFactory(BedrockFactory::class.'::create') From 2f4c8709d843e33520cbe0fa2f1ccf6dada13dd8 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:26:09 +0100 Subject: [PATCH 15/15] chore(cs): rollback assertEquals -> assertSame where was not intended --- src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php index 1edf80151..33a285fe3 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php @@ -37,7 +37,7 @@ public function testConvert(MessageBag $bag, array $expected) new UserMessageNormalizer(), ); - $this->assertSame($expected, $contract->createRequestPayload(new Nova('nova-pro'), $bag)); + $this->assertEquals($expected, $contract->createRequestPayload(new Nova('nova-pro'), $bag)); } /**