diff --git a/docs/bundles/ai-bundle.rst b/docs/bundles/ai-bundle.rst index 8e81f6258..f02135ef9 100644 --- a/docs/bundles/ai-bundle.rst +++ b/docs/bundles/ai-bundle.rst @@ -62,6 +62,7 @@ Advanced Example with Multiple Agents vertexai: location: '%env(GOOGLE_CLOUD_LOCATION)%' project_id: '%env(GOOGLE_CLOUD_PROJECT)%' + api_key: '%env(GOOGLE_CLOUD_VERTEX_API_KEY)%' # Needed only if authenticating with API keys ollama: host_url: '%env(OLLAMA_HOST_URL)%' agent: diff --git a/docs/components/platform/vertexai.rst b/docs/components/platform/vertexai.rst index 86bacf93a..ea28c472c 100644 --- a/docs/components/platform/vertexai.rst +++ b/docs/components/platform/vertexai.rst @@ -21,12 +21,12 @@ Setup Authentication ~~~~~~~~~~~~~~ -Vertex AI requires Google Cloud authentication. Follow the `Google cloud authentication guide`_ to set up your credentials. +Vertex AI supports 3 different authentication methods: -You can authenticate using: +1. Application Default Credentials (ADC) +---------------------------------------- -1. **Application Default Credentials (ADC)** - Recommended for production -2. **Service Account Key** - For development or specific service accounts +Follow the `Google cloud authentication guide`_ to set up your credentials. For ADC, install the Google Cloud SDK and authenticate: @@ -36,9 +36,6 @@ For ADC, install the Google Cloud SDK and authenticate: For detailed authentication setup, see `Setting up authentication for Vertex AI`_. -Environment Variables -~~~~~~~~~~~~~~~~~~~~~ - Configure your Google Cloud project and location: .. code-block:: bash @@ -46,8 +43,6 @@ Configure your Google Cloud project and location: GOOGLE_CLOUD_PROJECT=your-project-id GOOGLE_CLOUD_LOCATION=us-central1 -Usage ------ Basic usage example:: @@ -59,7 +54,7 @@ Basic usage example:: $platform = PlatformFactory::create( $_ENV['GOOGLE_CLOUD_LOCATION'], $_ENV['GOOGLE_CLOUD_PROJECT'], - $httpClient + httpClient: $httpClient ); $messages = new MessageBag( @@ -69,6 +64,31 @@ Basic usage example:: $result = $platform->invoke('gemini-2.5-flash', $messages); echo $result->getContent(); +2. Service Account Key +---------------------- + +Similar to the first approach, but instead of authenticating with the `gcloud` command, you provide the service account key directly using an environment variable: + +.. code-block:: bash + + GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json" + +3. API keys +----------- + +To get an API key, visit: `Vertex AI Studio (API keys)`_. + +Similar to the first approach, but instead of authenticating with the `gcloud` command, you provide the API keys when creating the Platform: + +Basic usage example with API keys:: + + $platform = PlatformFactory::create( + $_ENV['GOOGLE_CLOUD_LOCATION'], + $_ENV['GOOGLE_CLOUD_PROJECT'], + apiKey: $_ENV['GOOGLE_CLOUD_VERTEX_API_KEY'], + httpClient: $httpClient + ); + Model Availability by Location ------------------------------ @@ -164,3 +184,4 @@ See the ``examples/vertexai/`` directory for complete working examples: .. _Google cloud authentication guide: https://cloud.google.com/docs/authentication .. _Setting up authentication for Vertex AI: https://cloud.google.com/vertex-ai/docs/authentication .. _Google Cloud Console for Vertex AI: https://console.cloud.google.com/vertex-ai +.. _Vertex AI Studio (API keys): https://console.cloud.google.com/vertex-ai/studio/settings/api-keys diff --git a/examples/.env b/examples/.env index fce85f4b8..660444127 100644 --- a/examples/.env +++ b/examples/.env @@ -98,6 +98,7 @@ AIMLAPI_API_KEY= # Vertex AI GOOGLE_CLOUD_LOCATION=global GOOGLE_CLOUD_PROJECT=GOOGLE_CLOUD_PROJECT +GOOGLE_CLOUD_VERTEX_API_KEY= # For using Albert API (French Sovereign AI) ALBERT_API_KEY= diff --git a/examples/vertexai/audio-input.php b/examples/vertexai/audio-input.php index 7cc5dea59..ac1c5a96b 100644 --- a/examples/vertexai/audio-input.php +++ b/examples/vertexai/audio-input.php @@ -16,7 +16,7 @@ require_once __DIR__.'/bootstrap.php'; -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client()); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client()); $messages = new MessageBag( Message::ofUser( diff --git a/examples/vertexai/chat-with-api-key.php b/examples/vertexai/chat-with-api-key.php new file mode 100644 index 000000000..fc58a0924 --- /dev/null +++ b/examples/vertexai/chat-with-api-key.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\VertexAi\PlatformFactory; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; + +require_once __DIR__.'/bootstrap.php'; + +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), env('GOOGLE_CLOUD_VERTEX_API_KEY')); + +$messages = new MessageBag( + Message::forSystem('You are an expert assistant in geography.'), + Message::ofUser('Where is Mount Fuji?'), +); +$result = $platform->invoke('gemini-2.5-flash', $messages); + +echo $result->asText().\PHP_EOL; diff --git a/examples/vertexai/chat.php b/examples/vertexai/chat.php index 25e2ca5d1..b93c50e75 100644 --- a/examples/vertexai/chat.php +++ b/examples/vertexai/chat.php @@ -15,7 +15,7 @@ require_once __DIR__.'/bootstrap.php'; -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client()); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client()); $messages = new MessageBag( Message::forSystem('You are an expert assistant in geography.'), diff --git a/examples/vertexai/embeddings.php b/examples/vertexai/embeddings.php index 71187a29a..4288b254c 100644 --- a/examples/vertexai/embeddings.php +++ b/examples/vertexai/embeddings.php @@ -13,7 +13,7 @@ require_once __DIR__.'/bootstrap.php'; -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client()); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client()); $result = $platform->invoke('gemini-embedding-001', <<addSubscriber(new PlatformSubscriber()); -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client(), eventDispatcher: $dispatcher); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client(), eventDispatcher: $dispatcher); $clock = new Clock(new SymfonyClock()); $toolbox = new Toolbox([$clock], logger: logger()); diff --git a/examples/vertexai/structured-output-math.php b/examples/vertexai/structured-output-math.php index 16d9b748f..b54d7fdd8 100644 --- a/examples/vertexai/structured-output-math.php +++ b/examples/vertexai/structured-output-math.php @@ -21,7 +21,7 @@ $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new PlatformSubscriber()); -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client(), eventDispatcher: $dispatcher); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client(), eventDispatcher: $dispatcher); $messages = new MessageBag( Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'), Message::ofUser('how can I solve 8x + 7 = -23'), diff --git a/examples/vertexai/token-metadata.php b/examples/vertexai/token-metadata.php index e469b7b68..095432b7b 100644 --- a/examples/vertexai/token-metadata.php +++ b/examples/vertexai/token-metadata.php @@ -16,7 +16,7 @@ require_once __DIR__.'/bootstrap.php'; -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client()); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client()); $agent = new Agent($platform, 'gemini-2.0-flash-lite'); $messages = new MessageBag( diff --git a/examples/vertexai/toolcall.php b/examples/vertexai/toolcall.php index 7fe5c788a..fc8a24b16 100644 --- a/examples/vertexai/toolcall.php +++ b/examples/vertexai/toolcall.php @@ -19,7 +19,7 @@ require_once __DIR__.'/bootstrap.php'; -$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client()); +$platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), httpClient: adc_aware_http_client()); $toolbox = new Toolbox([new Clock()], logger: logger()); $processor = new AgentProcessor($toolbox); diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index adc0a656c..5d6d0da73 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -158,6 +158,7 @@ ->children() ->stringNode('location')->isRequired()->end() ->stringNode('project_id')->isRequired()->end() + ->stringNode('api_key')->defaultNull()->end() ->end() ->end() ->arrayNode('openai') diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 6810c6c03..8c025d490 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -586,6 +586,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB ->setArguments([ $platform['location'], $platform['project_id'], + $platform['api_key'] ?? null, $httpClient, new Reference('ai.platform.model_catalog.vertexai.gemini'), new Reference('ai.platform.contract.vertexai.gemini'), diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index c59c9f5f7..e51ed534c 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -89,9 +89,9 @@ public function testExtensionLoadDoesNotThrow() { $container = $this->buildContainer($this->getFullConfig()); - // Mock services that are used as platform create arguments, but should not be testet here or are not available. + // Mock services that are used as platform create arguments, but should not be tested here or are not available. $container->set('event_dispatcher', $this->createMock(EventDispatcherInterface::class)); - $container->getDefinition('ai.platform.vertexai')->replaceArgument(2, $this->createMock(HttpClientInterface::class)); + $container->getDefinition('ai.platform.vertexai')->replaceArgument(3, $this->createMock(HttpClientInterface::class)); $platforms = $container->findTaggedServiceIds('ai.platform'); @@ -7069,6 +7069,7 @@ private function getFullConfig(): array 'vertexai' => [ 'location' => 'global', 'project_id' => '123', + 'api_key' => 'vertex_key_full', ], 'dockermodelrunner' => [ 'host_url' => 'http://127.0.0.1:12434', diff --git a/src/platform/src/Bridge/VertexAi/Embeddings/ModelClient.php b/src/platform/src/Bridge/VertexAi/Embeddings/ModelClient.php index 8a0ae98da..918b1bb91 100644 --- a/src/platform/src/Bridge/VertexAi/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/VertexAi/Embeddings/ModelClient.php @@ -26,6 +26,7 @@ public function __construct( private readonly HttpClientInterface $httpClient, private readonly string $location, private readonly string $projectId, + private readonly ?string $apiKey = null, ) { } @@ -47,6 +48,11 @@ public function request(BaseModel $model, array|string $payload, array $options 'predict', ); + $query = []; + if (null !== $this->apiKey) { + $query['key'] = $this->apiKey; + } + $modelOptions = $model->getOptions(); $payload = [ @@ -71,6 +77,7 @@ public function request(BaseModel $model, array|string $payload, array $options 'Content-Type' => 'application/json', ], 'json' => array_merge($payload, $modelOptions), + 'query' => $query, ] ) ); diff --git a/src/platform/src/Bridge/VertexAi/Gemini/ModelClient.php b/src/platform/src/Bridge/VertexAi/Gemini/ModelClient.php index 70155f758..e6bef1836 100644 --- a/src/platform/src/Bridge/VertexAi/Gemini/ModelClient.php +++ b/src/platform/src/Bridge/VertexAi/Gemini/ModelClient.php @@ -30,6 +30,7 @@ public function __construct( HttpClientInterface $httpClient, private readonly string $location, private readonly string $projectId, + private readonly ?string $apiKey = null, ) { $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } @@ -52,6 +53,11 @@ public function request(BaseModel $model, array|string $payload, array $options $options['stream'] ?? false ? 'streamGenerateContent' : 'generateContent', ); + $query = []; + if (null !== $this->apiKey) { + $query['key'] = $this->apiKey; + } + if (isset($options[PlatformSubscriber::RESPONSE_FORMAT]['json_schema']['schema'])) { $options['generationConfig']['responseMimeType'] = 'application/json'; $options['generationConfig']['responseSchema'] = $options[PlatformSubscriber::RESPONSE_FORMAT]['json_schema']['schema']; @@ -107,6 +113,7 @@ public function request(BaseModel $model, array|string $payload, array $options $url, [ 'json' => array_merge($options, $payload), + 'query' => $query, ] ) ); diff --git a/src/platform/src/Bridge/VertexAi/PlatformFactory.php b/src/platform/src/Bridge/VertexAi/PlatformFactory.php index e80865edd..ff775739c 100644 --- a/src/platform/src/Bridge/VertexAi/PlatformFactory.php +++ b/src/platform/src/Bridge/VertexAi/PlatformFactory.php @@ -33,6 +33,7 @@ final class PlatformFactory public static function create( string $location, string $projectId, + #[\SensitiveParameter] ?string $apiKey = null, ?HttpClientInterface $httpClient = null, ModelCatalogInterface $modelCatalog = new ModelCatalog(), ?Contract $contract = null, @@ -45,7 +46,7 @@ public static function create( $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); return new Platform( - [new GeminiModelClient($httpClient, $location, $projectId), new EmbeddingsModelClient($httpClient, $location, $projectId)], + [new GeminiModelClient($httpClient, $location, $projectId, $apiKey), new EmbeddingsModelClient($httpClient, $location, $projectId, $apiKey)], [new GeminiResultConverter(), new EmbeddingsResultConverter()], $modelCatalog, $contract ?? GeminiContract::create(),