xbnz/google-gemini-api

HTTP clients for accessing Google's AI models and OAuth2 service

1.0.0 2024-05-23 02:49 UTC

This package is auto-updated.

Last update: 2024-06-12 23:32:53 UTC


README

Test suite Test suite Release License

Google Gemini API client with OAuth2 authentication

Motivation

Other libraries rely on the generativelanguage.googleapis.com API, which misses some models. For example, the newly-introduced "Gemini Experimental" model, which is free (!) cannot be accessed through that API. This library solves that issue.

Note

The Gemini Experimental API is not guaranteed to last forever. If you are using a model that is available on the generativelanguage.googleapis.com API, you can use this library to access it as well.

Installation

composer require xbnz/gemini

Prerequisites

Creating a service account on Google Cloud

  1. Go to the Google Cloud Console
  2. Go to IAM & Admin
  3. Go to Service Accounts
  4. Click on your project
  5. Click on "Create Service Account" if you don't have one yet
  6. Give it a name and click on "Create"
  7. Click on "Done"
  8. Go back into the service account you just created
  9. Click on "Add Key" and create a JSON key. The file will be downloaded to your computer
  10. Grab the necessary information from the JSON file:
    • client_email
    • private_key

Getting started

Sample application

Here is a sample Laravel command that uses this library. The service provider is where the interfaces are bound to concretes.

Registering to the dependency injection container (optional)

// Laravel example

namespace App\Providers;

clsss AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(GoogleOAuth2Interface::class, function (Application $app) {
            return new GoogleOAuth2Service(
                logger: $app->make(LoggerInterface::class)
            );
        });

        $this->app->bind(GoogleAIPlatformInterface::class, function (Application $app) {
            return new GoogleAIPlatformService(
                (new GoogleAIPlatformConnector(
                    $app->make('config')->get('services.google_ai_platform.project_id'),
                    $app->make('config')->get('services.google_ai_platform.region'),
                ))->authenticate(
                    new TokenAuthenticator(
                        $app->make(GoogleOAuth2Interface::class)->token(
                            new TokenRequestDTO(
                                googleServiceAccount: new GoogleServiceAccount(
                                    $app->make('config')->get('services.google_ai_platform.client_email'),
                                    $app->make('config')->get('services.google_ai_platform.private_key'),
                                ),
                                scope: 'https://www.googleapis.com/auth/cloud-platform',
                                issuedAt: CarbonImmutable::now(),
                                expiration: CarbonImmutable::now()->addHour()
                            )
                        )->accessToken
                    )
                ),
                $app->make('log')
            );
        });
    }
}

Using the AIPlatform service for single or multi-turn conversations

namespace App\Console\Commands;

class SomeCommand
{
    public function __construct(
        private readonly AIPlatformInterface $aiPlatformService
    ) {}

    public function handle(): void
    {
        $response = $this->aiPlatformService->generateContent(
            new GenerateContentRequestDTO(
                'publishers/google/models/gemini-experimental',
                Collection::make([
                    new ContentDTO(
                        Role::User,
                        Collection::make([
                            new TextPart('Explain what is happening in the image'),
                            new BlobPart(
                                'image/jpeg',
                                'base64 image...',
                            )
                        ])
                    ),
                    new ContentDTO(
                        Role::Model,
                        Collection::make([
                            new TextPart('Sure! This is an image of a cat.'),
                        ]),
                    ),
                    new ContentDTO(
                        Role::User,
                        Collection::make([
                            new TextPart('What color is the cat?'),
                        ]),
                    ),
                    // and so on...
                ])
                Collection::make([
                    new SafetySettings(HarmCategory::HarmCategoryHarassment, SafetyThreshold::BlockOnlyHigh),
                    new SafetySettings(HarmCategory::HarmCategoryHateSpeech, SafetyThreshold::BlockOnlyHigh),
                    new SafetySettings(HarmCategory::HarmCategorySexuallyExplicit, SafetyThreshold::BlockOnlyHigh),
                    new SafetySettings(HarmCategory::HarmCategoryDangerousContent, SafetyThreshold::BlockOnlyHigh),
                ]),
                systemInstructions: Collection::make([
                    new TextPart('Your instructions here...'),
                ]),
                generationConfig: new GenerationConfig(...) // Optional
            )
        );
        
        if ($response->finishReason->consideredSuccessful() === false) {
            // Handle the model not being able to generate content (probably due to unsafe content)
        }
        
        // Do something with the healthy response
        $response->usage->promptTokenCount;
        $modelResponse = $response
            ->content
            ->parts
            ->sole(fn(PartContract $part) => $part instanceof TextPart) // There should only be one TextPart::class in a model response (for now)
            ->text;
    }
}

Note

You may new up the classes directly if you don't want to use a dependency injection container

Note

You may serialize and store the TextPart::class responses from the model in your database. This will allow you to conduct a persistent chat session with the model, resembling something like teh ChatGPT interface.

Core concepts

Extensible authentication

The Google aiplatform.googleapis.com API allows several forms of authentication. This library is opinionated toward using the access_token derived from your service account. However, the underlying HTTP client of this library (SaloonPHP) allows "Connector" classes to be extended with any authenticator before being newed up. As a result, you may create your own Saloon authentication strategy and pass it to the constructor:

namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(GoogleAIPlatformInterface::class, function (Application $app) {
            return new GoogleAIPlatformService(
                (new GoogleAIPlatformConnector(...))->authenticate(new MyCustomAuthenticator),
            );
        });
    }
}

Bearer token authentication

The provided GoogleOAuthInterface::token() method allows you to fetch a new token for bearer authentication. Do keep in mind that if you use this tool for authenticating the AIPlatform, the process generates a new token every time the method is called which may or may not be desirable.

Caching your token until expiry (Laravel example)

This package does not provide a caching mechanism – for example using the PSR cache interface – due to the dead simple cache integration of most modern frameworks. Here is a Laravel example:

namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
          $this->app->bind(GoogleAIPlatformInterface::class, function (Application $app) {
            return new GoogleAIPlatformService(
                (new GoogleAIPlatformConnector(...))->authenticate(
                    new TokenAuthenticator(
                        $app->make('cache')->remember(
                            'google_api_platform_token',
                            CarbonImmutable::now()->addMinutes(60),
                            function () use ($app) {
                                return $app->make(GoogleOAuth2Interface::class)->token(
                                    new TokenRequestDTO(
                                        googleServiceAccount: new GoogleServiceAccount(
                                            $app->make('config')->get('services.google_ai_platform.client_email'),
                                            $app->make('config')->get('services.google_ai_platform.private_key'),
                                        ),
                                        scope: 'https://www.googleapis.com/auth/cloud-platform',
                                        issuedAt: CarbonImmutable::now(),
                                        expiration: CarbonImmutable::now()->addHour()
                                    )
                                )->accessToken;
                            }
                        ),
                    )
                ),
                $app->make('log')
            );
        });
    }
}

Logging and exception handling

Providing your own PSR-compatible logger

This library uses the PSR logger interface for logging. You may provide your own logger by passing it to the constructor of the service:

namespace App\Console\Commands;

class SomeCommand
{
    public function handle(): void
    {
        $service = new GoogleAIPlatformService(
            new GoogleAIPlatformConnector(...),
            new MyCustomLogger
        );
        
        try {
            $response = $service->generateContent(...);
        } catch (GoogleAIPlatformException $e) {
            // Handle the exception
        }
        
        // At this point, the logger will have logged the exception
    }
}

The same functionality exists on the OAuth2 service.

Lifecycle hooks for requests and responses

This is not recommended for basic use cases. However, APIs change and maintainers are lazy. If Google adds a new field to a response which I cannot work on, you are free to hook into the response and create your own DTOs if you don't want to make a PR.

namespace App\Console\Commands;

class SomeCommand
{
    public function __construct(
        private readonly GoogleAIPlatformInterface $aiPlatform
    ) {}

    public function handle(): void
    {
        $this->aiPlatform->generateContent(
            requestDto: ...,
            beforeRequest: function (Request $request) {
                $request->headers()->merge([
                    'X-Custom-Header' => 'Value'
                ]);
                
                return $request;
            },
            afterResponse: function (Response $response) {
                // Return your own DTO or do something with the response
                
                return $response;
            }
        )
    }
}

Retrying failed requests

When using free Gemini models, strict rate limits are imposed. You may decide to retry when 429 responses are encountered.

Prerequisites

A Guzzle retry middleware is not provided. You may implement your own or use a library of your choice. Example:

composer require caseyamcl/guzzle_retry_middleware
namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(GoogleAIPlatformInterface::class, function (Application $app) {
            $connector = new GoogleAIPlatformConnector(...);
            $connector->authenticate(...)
            $connector->sender()->getHandlerStack()->push(
                GuzzleRetryMiddleware::factory([
                    'max_retry_attempts' => 5,
                    'retry_on_status' => [429],
                ])
            )
        
            return new GoogleAIPlatformService(
                $connector
            );
        });
    }
}

Testing

This library provides fake implementations for testing purposes. For example, you may use the GoogleOAuth2ServiceFake::class like so:

Calling code

namespace App\Console\Commands;

use XbNz\Gemini\OAuth2\GoogleOAuth2Interface;

class SomeCommand
{
    public function __construct(
        private readonly GoogleOAuth2Interface $googleOAuth2Service
    ) {}

    public function handle(): void
    {
        $response = $this->googleOAuth2Service->token(...);
    }
}

Test code

namespace Tests\Feature;

use Tests\TestCase;
use XbNz\Gemini\OAuth2\DataTransferObjects\Requests\TokenRequestDTO;
use XbNz\Gemini\OAuth2\GoogleOAuth2ServiceFake;

class SomeCommandTest extends TestCase
{
    public function test_it_works(): void
    {
        // Swapping the real service with the fake one in the Laravel container
        $this->app->swap(GoogleOAuth2Interface::class, $fake = new GoogleOAuth2ServiceFake);
        
        // Basic testing helpers
        $fake->alwaysReturnToken(...);
        $fake->assertTokenRequestCount(...);
        
        // Asserting against requests
        $fake->assertTokenRequest(function (TokenRequestDTO) {
            return $dto->grantType === 'urn:ietf:params:oauth:grant-type:jwt-bearer';
        });
    }
}

Note

The same concept can be applied to the AIPlatform service. The fake implementation is called GoogleAIPlatformServiceFake::class and provides similar test assertions