From 0f9f2145d3e03d760825657c8bc10bf96f4af9a1 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 bcbc3df5da..676f79ea84 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 a09626f745..22087db790 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; @@ -95,6 +96,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 82ed76d17d..2216089105 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; @@ -482,6 +483,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 2d7632fe09..a28d874e02 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 46776deab5bd3e9460baef7f66912529fa3ec065 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 a1990da623..31329a0c42 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 3dfee67a37d9a15dd841f01bc1dd56d6c17598a7 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 16:31:59 +0100 Subject: [PATCH 03/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 2216089105..6e8b4ba4fc 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -470,7 +470,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 4305da5ee6ea8eb8b4c768588cb18c3177606b8d Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 17:35:39 +0100 Subject: [PATCH 04/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 676f79ea84..1c2c4a82ad 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 6e8b4ba4fc..1da470163f 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -470,7 +470,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'), @@ -495,7 +495,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 ab03e96ff5..3e0ffb7622 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; @@ -7062,6 +7063,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); @@ -7098,6 +7100,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 2f22dd1efc3b7fb2f5b3e29a85cd3cb2d7e91ff3 Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 18:45:02 +0100 Subject: [PATCH 05/15] chore(bedrock): cover model clients with tests --- .../Tests/Nova/NovaModelClientTest.php | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php 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 0000000000..423b4b0589 --- /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 e3d54a5ff6084c82a3a5a017fbaeb5c977ffb8ee Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 20:22:10 +0100 Subject: [PATCH 06/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 | 8 ++++---- src/ai-bundle/tests/DependencyInjection/AiBundleTest.php | 7 ++++--- src/platform/src/Bridge/Bedrock/PlatformFactory.php | 6 +++++- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/bundles/ai-bundle.rst b/docs/bundles/ai-bundle.rst index b45ae64eaf..aa6af9d6d3 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 1c2c4a82ad..40b00157fa 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 1da470163f..a444c5bf81 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -495,12 +495,12 @@ private function processPlatformConfig(string $type, array $platform, ContainerB ->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 3e0ffb7622..84dbf7fa1e 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -7063,7 +7063,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); @@ -7101,8 +7101,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 a28d874e02..999bb5320a 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 e2e379cf9683c39083778b1d6f4b9f75a96946ca Mon Sep 17 00:00:00 2001 From: uerka Date: Thu, 18 Dec 2025 20:46:09 +0100 Subject: [PATCH 07/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 a444c5bf81..dc378959b6 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -484,8 +484,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 b51b429d773c5debf0e49ea0dc9a4ee14749f450 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:00:53 +0100 Subject: [PATCH 08/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 999bb5320a..be2af84852 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 98ad225181950166fc5bd7e2431578a8d8c6e509 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:04:05 +0100 Subject: [PATCH 09/15] chore(cs): assertEquals -> assertSame --- .../Bedrock/Tests/Nova/ContractTest.php | 2 +- .../Tests/Nova/NovaModelClientTest.php | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php index cec3cb4566..444554ec3f 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php @@ -40,7 +40,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 423b4b0589..f569efe881 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 79116b656e5ded616fdebf580e0764adee42afd7 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:04:16 +0100 Subject: [PATCH 10/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 aa6af9d6d3..6aec487eb7 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 9f055a485687ea132378093964dee2a5bc1ea049 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:10:09 +0100 Subject: [PATCH 11/15] fix(bundle): check for bedrock platform package only in case at least one configured --- src/ai-bundle/src/AiBundle.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index dc378959b6..30cec8d0a9 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -484,11 +484,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') @@ -500,7 +500,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB null, new Reference('event_dispatcher'), ]) - ->addTag('ai.platform', ['name' => 'bedrock .'.$name]); + ->addTag('ai.platform', ['name' => 'bedrock.'.$name]); $container->setDefinition($platformId, $definition); } From 349da4cbaf5b4654d15b13f6813cc34864507ff8 Mon Sep 17 00:00:00 2001 From: uerka Date: Sat, 20 Dec 2025 16:26:09 +0100 Subject: [PATCH 12/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 444554ec3f..cec3cb4566 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/ContractTest.php @@ -40,7 +40,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)); } /** From ffd15bc96c77ba7bd99d1cc3dcbd6ccfd4f1245d Mon Sep 17 00:00:00 2001 From: uerka Date: Sun, 21 Dec 2025 12:12:22 +0100 Subject: [PATCH 13/15] chore(docs): change to ai.platform.bedrock.{name} for agent definition --- 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 6aec487eb7..8eca15ab7f 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 3f48482e81685a6b24146617d9445eea1602ed0b Mon Sep 17 00:00:00 2001 From: uerka Date: Sun, 21 Dec 2025 12:33:04 +0100 Subject: [PATCH 14/15] chore(cs): fixing cs --- .../Bedrock/Tests/Nova/NovaModelClientTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php index f569efe881..6ab1c450ac 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); @@ -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); @@ -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); @@ -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); From ba8666065e5965f4b1b4f1141e3068a94a119ae4 Mon Sep 17 00:00:00 2001 From: uerka Date: Sun, 21 Dec 2025 12:38:03 +0100 Subject: [PATCH 15/15] chore(cs): fixing cs --- .../src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php index 6ab1c450ac..0f1195e768 100644 --- a/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php +++ b/src/platform/src/Bridge/Bedrock/Tests/Nova/NovaModelClientTest.php @@ -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); @@ -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); @@ -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); @@ -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);