├── .github └── workflows │ ├── code-quality.yml │ └── tests.yml ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── assets └── example.png ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── src ├── ChatSession.php ├── Client.php ├── ClientInterface.php ├── EmbeddingModel.php ├── Enums │ ├── BlockReason.php │ ├── FinishReason.php │ ├── HarmBlockThreshold.php │ ├── HarmCategory.php │ ├── HarmProbability.php │ ├── MimeType.php │ ├── ModelName.php │ ├── Role.php │ └── TaskType.php ├── GenerationConfig.php ├── GenerativeModel.php ├── Json │ └── ObjectListParser.php ├── Requests │ ├── CountTokensRequest.php │ ├── EmbedContentRequest.php │ ├── GenerateContentRequest.php │ ├── GenerateContentStreamRequest.php │ ├── ListModelsRequest.php │ └── RequestInterface.php ├── Resources │ ├── Candidate.php │ ├── CitationMetadata.php │ ├── CitationSource.php │ ├── Content.php │ ├── ContentEmbedding.php │ ├── Model.php │ ├── ModelName.php │ ├── Parts │ │ ├── FilePart.php │ │ ├── ImagePart.php │ │ ├── PartInterface.php │ │ └── TextPart.php │ ├── PromptFeedback.php │ └── SafetyRating.php ├── Responses │ ├── CountTokensResponse.php │ ├── EmbedContentResponse.php │ ├── GenerateContentResponse.php │ └── ListModelsResponse.php ├── SafetySetting.php └── Traits │ ├── ArrayTypeValidator.php │ └── ModelNameToString.php └── tests └── Unit ├── ClientTest.php ├── Requests ├── CountTokensRequestTest.php ├── EmbedContentRequestTest.php ├── GenerateContentRequestTest.php └── ListModelsRequestTest.php └── Resources ├── CandidateTest.php ├── CitationMetadataTest.php ├── CitationSourceTest.php ├── ContentEmbeddingTest.php ├── ContentTest.php └── Parts ├── ImagePartTest.php └── TextPartTest.php /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: ['push'] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | php-version: [8.1] 13 | 14 | name: Code Quality 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Cache dependencies 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.composer/cache/files 24 | key: php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }} 25 | 26 | - name: Setup PHP 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: ${{ matrix.php-version }} 30 | coverage: none 31 | 32 | - name: Install Composer dependencies 33 | run: composer update --no-interaction --prefer-stable --prefer-dist 34 | 35 | - name: Code Style Checks 36 | run: composer test:style 37 | 38 | - name: Types Checks 39 | run: composer test:types 40 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: ['push'] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | php-version: [8.1, 8.2, 8.3, 8.4] 13 | dependency-version: [prefer-lowest, prefer-stable] 14 | 15 | name: Tests PHP${{ matrix.php-version }} - ${{ matrix.dependency-version }} 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Cache dependencies 22 | uses: actions/cache@v1 23 | with: 24 | path: ~/.composer/cache/files 25 | key: php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }} 26 | 27 | - name: Setup PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php-version }} 31 | coverage: xdebug 32 | 33 | - name: Install Composer dependencies 34 | run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist 35 | 36 | - name: Unit Tests 37 | run: composer test:unit-with-coverage 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | /composer.lock 4 | /.php-cs-fixer.cache 5 | /.phpunit.cache/ 6 | .idea 7 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ; 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setRules([ 9 | '@PSR12' => true, 10 | ]) 11 | ->setFinder($finder) 12 | ; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 gemini-api-php/client 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Gemini API PHP Client - Example 3 |

4 |

5 | Total Downloads 6 | Latest Version 7 | License 8 |

9 | 10 | # Gemini API PHP Client 11 | 12 | Gemini API PHP Client allows you to use the Google's generative AI models, like Gemini Pro and Gemini Pro Vision. 13 | 14 | _This library is not developed or endorsed by Google._ 15 | 16 | - Erdem Köse - **[github.com/erdemkose](https://github.com/erdemkose)** 17 | 18 | ## Table of Contents 19 | - [Installation](#installation) 20 | - [How to use](#how-to-use) 21 | - [Basic text generation](#basic-text-generation) 22 | - [Text generation with system instruction](#text-generation-with-system-instruction) 23 | - [Multimodal input](#multimodal-input) 24 | - [Chat Session (Multi-Turn Conversations)](#chat-session-multi-turn-conversations) 25 | - [Chat Session with history](#chat-session-with-history) 26 | - [Streaming responses](#streaming-responses) 27 | - [Streaming Chat Session](#streaming-chat-session) 28 | - [Tokens counting](#tokens-counting) 29 | - [Listing models](#listing-models) 30 | - [Advanced Usages](#advanced-usages) 31 | - [Using Beta version](#using-beta-version) 32 | - [Safety Settings and Generation Configuration](#safety-settings-and-generation-configuration) 33 | - [Using your own HTTP client](#using-your-own-http-client) 34 | - [Using your own HTTP client for streaming responses](#using-your-own-http-client-for-streaming-responses) 35 | 36 | ## Installation 37 | 38 | > You need an API key to gain access to Google's Gemini API. 39 | > Visit [Google AI Studio](https://makersuite.google.com/) to get an API key. 40 | 41 | First step is to install the Gemini API PHP client with Composer. 42 | 43 | ```shell 44 | composer require gemini-api-php/client 45 | ``` 46 | 47 | Gemini API PHP client does not come with an HTTP client. 48 | If you are just testing or do not have an HTTP client library in your project, 49 | you need to allow `php-http/discovery` composer plugin or install a PSR-18 compatible client library. 50 | 51 | ## How to use 52 | 53 | ### Basic text generation 54 | 55 | ```php 56 | use GeminiAPI\Client; 57 | use GeminiAPI\Resources\ModelName; 58 | use GeminiAPI\Resources\Parts\TextPart; 59 | 60 | $client = new Client('GEMINI_API_KEY'); 61 | $response = $client->generativeModel(ModelName::GEMINI_PRO)->generateContent( 62 | new TextPart('PHP in less than 100 chars'), 63 | ); 64 | 65 | print $response->text(); 66 | // PHP: A server-side scripting language used to create dynamic web applications. 67 | // Easy to learn, widely used, and open-source. 68 | ``` 69 | 70 | ### Text generation with system instruction 71 | 72 | > System instruction is currently supported only in beta version 73 | 74 | ```php 75 | use GeminiAPI\Client; 76 | use GeminiAPI\Resources\ModelName; 77 | use GeminiAPI\Resources\Parts\TextPart; 78 | 79 | $client = new Client('GEMINI_API_KEY'); 80 | $response = $client->withV1BetaVersion() 81 | ->generativeModel(ModelName::GEMINI_1_5_FLASH) 82 | ->withSystemInstruction('You are a cat. Your name is Neko.') 83 | ->generateContent( 84 | new TextPart('PHP in less than 100 chars'), 85 | ); 86 | 87 | print $response->text(); 88 | // Meow? Purrfectly concise, wouldn't you say? *Stretches luxuriously* 89 | ``` 90 | 91 | ### Multimodal input 92 | 93 | > Image input modality is only enabled for Gemini Pro Vision model 94 | 95 | ```php 96 | use GeminiAPI\Client; 97 | use GeminiAPI\Enums\MimeType; 98 | use GeminiAPI\Resources\ModelName; 99 | use GeminiAPI\Resources\Parts\ImagePart; 100 | use GeminiAPI\Resources\Parts\TextPart; 101 | 102 | $client = new Client('GEMINI_API_KEY'); 103 | $response = $client->generativeModel(ModelName::GEMINI_PRO)->generateContent( 104 | new TextPart('Explain what is in the image'), 105 | new ImagePart( 106 | MimeType::IMAGE_JPEG, 107 | base64_encode(file_get_contents('elephpant.jpg')), 108 | ), 109 | ); 110 | 111 | print $response->text(); 112 | // The image shows an elephant standing on the Earth. 113 | // The elephant is made of metal and has a glowing symbol on its forehead. 114 | // The Earth is surrounded by a network of glowing lines. 115 | // The image is set against a starry background. 116 | ``` 117 | 118 | ### Chat Session (Multi-Turn Conversations) 119 | 120 | ```php 121 | use GeminiAPI\Client; 122 | use GeminiAPI\Resources\ModelName; 123 | use GeminiAPI\Resources\Parts\TextPart; 124 | 125 | $client = new Client('GEMINI_API_KEY'); 126 | $chat = $client->generativeModel(ModelName::GEMINI_PRO)->startChat(); 127 | 128 | $response = $chat->sendMessage(new TextPart('Hello World in PHP')); 129 | print $response->text(); 130 | 131 | $response = $chat->sendMessage(new TextPart('in Go')); 132 | print $response->text(); 133 | ``` 134 | 135 | ```text 136 | 139 | 140 | This code will print "Hello World!" to the standard output. 141 | ``` 142 | 143 | ```text 144 | package main 145 | 146 | import "fmt" 147 | 148 | func main() { 149 | fmt.Println("Hello World!") 150 | } 151 | 152 | This code will print "Hello World!" to the standard output. 153 | ``` 154 | 155 | ### Chat Session with history 156 | 157 | ```php 158 | use GeminiAPI\Client; 159 | use GeminiAPI\Enums\Role; 160 | use GeminiAPI\Resources\Content; 161 | use GeminiAPI\Resources\ModelName; 162 | use GeminiAPI\Resources\Parts\TextPart; 163 | 164 | $history = [ 165 | Content::text('Hello World in PHP', Role::User), 166 | Content::text( 167 | << 171 | 172 | This code will print "Hello World!" to the standard output. 173 | TEXT, 174 | Role::Model, 175 | ), 176 | ]; 177 | 178 | $client = new Client('GEMINI_API_KEY'); 179 | $chat = $client->generativeModel(ModelName::GEMINI_PRO) 180 | ->startChat() 181 | ->withHistory($history); 182 | 183 | $response = $chat->sendMessage(new TextPart('in Go')); 184 | print $response->text(); 185 | ``` 186 | 187 | ```text 188 | package main 189 | 190 | import "fmt" 191 | 192 | func main() { 193 | fmt.Println("Hello World!") 194 | } 195 | 196 | This code will print "Hello World!" to the standard output. 197 | ``` 198 | 199 | ### Streaming responses 200 | 201 | > Requires `curl` extension to be enabled 202 | 203 | In the streaming response, the callback function will be called whenever a response is returned from the server. 204 | 205 | Long responses may be broken into separate responses, and you can start receiving responses faster using a content stream. 206 | 207 | ```php 208 | use GeminiAPI\Client; 209 | use GeminiAPI\Resources\ModelName; 210 | use GeminiAPI\Resources\Parts\TextPart; 211 | use GeminiAPI\Responses\GenerateContentResponse; 212 | 213 | $callback = function (GenerateContentResponse $response): void { 214 | static $count = 0; 215 | 216 | print "\nResponse #{$count}\n"; 217 | print $response->text(); 218 | $count++; 219 | }; 220 | 221 | $client = new Client('GEMINI_API_KEY'); 222 | $client->generativeModel(ModelName::GEMINI_PRO)->generateContentStream( 223 | $callback, 224 | [new TextPart('PHP in less than 100 chars')], 225 | ); 226 | // Response #0 227 | // PHP: a versatile, general-purpose scripting language for web development, popular for 228 | // Response #1 229 | // its simple syntax and rich library of functions. 230 | ``` 231 | 232 | ### Streaming Chat Session 233 | 234 | > Requires `curl` extension to be enabled 235 | 236 | ```php 237 | use GeminiAPI\Client; 238 | use GeminiAPI\Enums\Role; 239 | use GeminiAPI\Resources\Content; 240 | use GeminiAPI\Resources\ModelName; 241 | use GeminiAPI\Resources\Parts\TextPart; 242 | use GeminiAPI\Responses\GenerateContentResponse; 243 | 244 | $history = [ 245 | Content::text('Hello World in PHP', Role::User), 246 | Content::text( 247 | << 251 | 252 | This code will print "Hello World!" to the standard output. 253 | TEXT, 254 | Role::Model, 255 | ), 256 | ]; 257 | 258 | $callback = function (GenerateContentResponse $response): void { 259 | static $count = 0; 260 | 261 | print "\nResponse #{$count}\n"; 262 | print $response->text(); 263 | $count++; 264 | }; 265 | 266 | $client = new Client('GEMINI_API_KEY'); 267 | $chat = $client->generativeModel(ModelName::GEMINI_PRO) 268 | ->startChat() 269 | ->withHistory($history); 270 | 271 | $chat->sendMessageStream($callback, new TextPart('in Go')); 272 | ``` 273 | 274 | ```text 275 | Response #0 276 | package main 277 | 278 | import "fmt" 279 | 280 | func main() { 281 | 282 | Response #1 283 | fmt.Println("Hello World!") 284 | } 285 | 286 | This code will print "Hello World!" to the standard output. 287 | ``` 288 | 289 | ### Embed Content 290 | 291 | ```php 292 | use GeminiAPI\Client; 293 | use GeminiAPI\Resources\ModelName; 294 | use GeminiAPI\Resources\Parts\TextPart; 295 | 296 | $client = new Client('GEMINI_API_KEY'); 297 | $response = $client->embeddingModel(ModelName::EMBEDDING_001) 298 | ->embedContent( 299 | new TextPart('PHP in less than 100 chars'), 300 | ); 301 | 302 | print_r($response->embedding->values); 303 | // [ 304 | // [0] => 0.041395925 305 | // [1] => -0.017692696 306 | // ... 307 | // ] 308 | ``` 309 | 310 | ### Tokens counting 311 | 312 | ```php 313 | use GeminiAPI\Client; 314 | use GeminiAPI\Resources\ModelName; 315 | use GeminiAPI\Resources\Parts\TextPart; 316 | 317 | $client = new Client('GEMINI_API_KEY'); 318 | $response = $client->generativeModel(ModelName::GEMINI_PRO)->countTokens( 319 | new TextPart('PHP in less than 100 chars'), 320 | ); 321 | 322 | print $response->totalTokens; 323 | // 10 324 | ``` 325 | 326 | ### Listing models 327 | 328 | ```php 329 | use GeminiAPI\Client; 330 | 331 | $client = new Client('GEMINI_API_KEY'); 332 | $response = $client->listModels(); 333 | 334 | print_r($response->models); 335 | //[ 336 | // [0] => GeminiAPI\Resources\Model Object 337 | // ( 338 | // [name] => models/gemini-pro 339 | // [displayName] => Gemini Pro 340 | // [description] => The best model for scaling across a wide range of tasks 341 | // ... 342 | // ) 343 | // [1] => GeminiAPI\Resources\Model Object 344 | // ( 345 | // [name] => models/gemini-pro-vision 346 | // [displayName] => Gemini Pro Vision 347 | // [description] => The best image understanding model to handle a broad range of applications 348 | // ... 349 | // ) 350 | //] 351 | ``` 352 | 353 | ### Advanced Usages 354 | 355 | #### Using Beta version 356 | 357 | ```php 358 | use GeminiAPI\Client; 359 | use GeminiAPI\Resources\ModelName; 360 | use GeminiAPI\Resources\Parts\TextPart; 361 | 362 | $client = (new Client('GEMINI_API_KEY')) 363 | ->withV1BetaVersion(); 364 | $response = $client->generativeModel(ModelName::GEMINI_PRO)->countTokens( 365 | new TextPart('PHP in less than 100 chars'), 366 | ); 367 | 368 | print $response->totalTokens; 369 | // 10 370 | ``` 371 | 372 | #### Safety Settings and Generation Configuration 373 | 374 | ```php 375 | use GeminiAPI\Client; 376 | use GeminiAPI\Enums\HarmCategory; 377 | use GeminiAPI\Enums\HarmBlockThreshold; 378 | use GeminiAPI\GenerationConfig; 379 | use GeminiAPI\Resources\ModelName; 380 | use GeminiAPI\Resources\Parts\TextPart; 381 | use GeminiAPI\SafetySetting; 382 | 383 | $safetySetting = new SafetySetting( 384 | HarmCategory::HARM_CATEGORY_HATE_SPEECH, 385 | HarmBlockThreshold::BLOCK_LOW_AND_ABOVE, 386 | ); 387 | $generationConfig = (new GenerationConfig()) 388 | ->withCandidateCount(1) 389 | ->withMaxOutputTokens(40) 390 | ->withTemperature(0.5) 391 | ->withTopK(40) 392 | ->withTopP(0.6) 393 | ->withStopSequences(['STOP']); 394 | 395 | $client = new Client('GEMINI_API_KEY'); 396 | $response = $client->generativeModel(ModelName::GEMINI_PRO) 397 | ->withAddedSafetySetting($safetySetting) 398 | ->withGenerationConfig($generationConfig) 399 | ->generateContent( 400 | new TextPart('PHP in less than 100 chars') 401 | ); 402 | ``` 403 | 404 | #### Using your own HTTP client 405 | 406 | ```php 407 | use GeminiAPI\Client as GeminiClient; 408 | use GeminiAPI\Resources\ModelName; 409 | use GeminiAPI\Resources\Parts\TextPart; 410 | use GuzzleHttp\Client as GuzzleClient; 411 | 412 | $guzzle = new GuzzleClient([ 413 | 'proxy' => 'http://localhost:8125', 414 | ]); 415 | 416 | $client = new GeminiClient('GEMINI_API_KEY', $guzzle); 417 | $response = $client->generativeModel(ModelName::GEMINI_PRO)->generateContent( 418 | new TextPart('PHP in less than 100 chars') 419 | ); 420 | ``` 421 | 422 | #### Using your own HTTP client for streaming responses 423 | 424 | > Requires `curl` extension to be enabled 425 | 426 | Since streaming responses are fetched using `curl` extension, they cannot use the custom HTTP client passed to the Gemini Client. 427 | You need to pass a `CurlHandler` if you want to override connection options. 428 | 429 | The following curl options will be overwritten by the Gemini Client. 430 | 431 | - `CURLOPT_URL` 432 | - `CURLOPT_POST` 433 | - `CURLOPT_POSTFIELDS` 434 | - `CURLOPT_WRITEFUNCTION` 435 | 436 | You can also pass the headers you want to be used in the requests. 437 | 438 | ```php 439 | use GeminiAPI\Client; 440 | use GeminiAPI\Resources\ModelName; 441 | use GeminiAPI\Resources\Parts\TextPart; 442 | use GeminiAPI\Responses\GenerateContentResponse; 443 | 444 | $callback = function (GenerateContentResponse $response): void { 445 | print $response->text(); 446 | }; 447 | 448 | $ch = curl_init(); 449 | curl_setopt($ch, \CURLOPT_PROXY, 'http://localhost:8125'); 450 | 451 | $client = new Client('GEMINI_API_KEY'); 452 | $client->withRequestHeaders([ 453 | 'User-Agent' => 'My Gemini-backed app' 454 | ]) 455 | ->generativeModel(ModelName::GEMINI_PRO) 456 | ->generateContentStream( 457 | $callback, 458 | [new TextPart('PHP in less than 100 chars')], 459 | $ch, 460 | ); 461 | ``` 462 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-api-php/client/a48e61285d82b24117a5c8928dd1e504818f908b/assets/example.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gemini-api-php/client", 3 | "description": "API client for Google's Gemini API", 4 | "keywords": [ 5 | "php", 6 | "client", 7 | "sdk", 8 | "api", 9 | "google", 10 | "gemini", 11 | "gemini pro", 12 | "gemini pro vision", 13 | "ai" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Erdem Köse", 19 | "email": "erdemkose@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "php-http/discovery": "^1.19", 25 | "psr/http-client": "^1.0", 26 | "psr/http-client-implementation": "*", 27 | "psr/http-factory": "^1.0.2", 28 | "psr/http-factory-implementation": "*", 29 | "psr/http-message": "^1.0.1 || ^2.0" 30 | }, 31 | "require-dev": { 32 | "friendsofphp/php-cs-fixer": "^3.41", 33 | "guzzlehttp/guzzle": "^7.8.0", 34 | "guzzlehttp/psr7": "^2.0.0", 35 | "phpstan/phpstan": "^1.10.50", 36 | "phpunit/phpunit": "^10.5" 37 | }, 38 | "suggest": { 39 | "ext-curl": "Required for streaming responses" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "GeminiAPI\\": "src/" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "GeminiAPI\\Tests\\": "tests/" 49 | } 50 | }, 51 | "minimum-stability": "stable", 52 | "prefer-stable": true, 53 | "config": { 54 | "sort-packages": true, 55 | "preferred-install": "dist", 56 | "allow-plugins": { 57 | "php-http/discovery": false 58 | } 59 | }, 60 | "scripts": { 61 | "fix:style": "php-cs-fixer fix . -vv", 62 | "test:style": "php-cs-fixer check . -vv", 63 | "test:types": "phpstan analyse --ansi", 64 | "test:unit": "phpunit --testsuite unit", 65 | "test:unit-with-coverage": "phpunit --testsuite unit --coverage-text", 66 | "test": [ 67 | "@test:style", 68 | "@test:types", 69 | "@test:unit" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | treatPhpDocTypesAsCertain: false 6 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./src 9 | 10 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/ChatSession.php: -------------------------------------------------------------------------------- 1 | history[] = new Content($parts, Role::User); 33 | 34 | $config = (new GenerationConfig()) 35 | ->withCandidateCount(1); 36 | $response = $this->model 37 | ->withGenerationConfig($config) 38 | ->generateContentWithContents($this->history); 39 | 40 | if (!empty($response->candidates)) { 41 | $parts = $response->candidates[0]->content->parts; 42 | $this->history[] = new Content($parts, Role::Model); 43 | } 44 | 45 | return $response; 46 | } 47 | 48 | /** 49 | * @param callable(GenerateContentResponse): void $callback 50 | * @param PartInterface ...$parts 51 | * @return void 52 | */ 53 | public function sendMessageStream( 54 | callable $callback, 55 | PartInterface ...$parts, 56 | ): void { 57 | $this->history[] = new Content($parts, Role::User); 58 | 59 | $parts = []; 60 | $partsCollectorCallback = function (GenerateContentResponse $response) use ($callback, &$parts) { 61 | if (!empty($response->candidates)) { 62 | array_push($parts, ...$response->parts()); 63 | } 64 | 65 | $callback($response); 66 | }; 67 | 68 | $config = (new GenerationConfig()) 69 | ->withCandidateCount(1); 70 | $this->model 71 | ->withGenerationConfig($config) 72 | ->generateContentStreamWithContents($partsCollectorCallback, $this->history); 73 | 74 | if (!empty($parts)) { 75 | $this->history[] = new Content($parts, Role::Model); 76 | } 77 | } 78 | 79 | /** 80 | * @return Content[] 81 | */ 82 | public function history(): array 83 | { 84 | return $this->history; 85 | } 86 | 87 | /** 88 | * @param Content[] $history 89 | * @return $this 90 | * @throws InvalidArgumentException 91 | */ 92 | public function withHistory(array $history): self 93 | { 94 | $this->ensureArrayOfType($history, Content::class); 95 | 96 | $clone = clone $this; 97 | $clone->history = $history; 98 | 99 | return $clone; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 47 | */ 48 | private array $requestHeaders = []; 49 | 50 | public function __construct( 51 | private readonly string $apiKey, 52 | private ?HttpClientInterface $client = null, 53 | private ?RequestFactoryInterface $requestFactory = null, 54 | private ?StreamFactoryInterface $streamFactory = null, 55 | ) { 56 | $this->client ??= Psr18ClientDiscovery::find(); 57 | $this->requestFactory ??= Psr17FactoryDiscovery::findRequestFactory(); 58 | $this->streamFactory ??= Psr17FactoryDiscovery::findStreamFactory(); 59 | } 60 | 61 | public function geminiPro(): GenerativeModel 62 | { 63 | return $this->generativeModel(ModelName::GeminiPro); 64 | } 65 | 66 | public function geminiProVision(): GenerativeModel 67 | { 68 | return $this->generativeModel(ModelName::GeminiProVision); 69 | } 70 | 71 | public function geminiPro10(): GenerativeModel 72 | { 73 | return $this->generativeModel(ModelName::GeminiPro10); 74 | } 75 | public function geminiPro10Latest(): GenerativeModel 76 | { 77 | return $this->generativeModel(ModelName::GeminiPro10Latest); 78 | } 79 | 80 | public function geminiPro15(): GenerativeModel 81 | { 82 | return $this->generativeModel(ModelName::GeminiPro15); 83 | } 84 | 85 | public function geminiProFlash1_5(): GenerativeModel 86 | { 87 | return $this->generativeModel(ModelName::GeminiPro15Flash); 88 | } 89 | 90 | 91 | public function generativeModel(ModelName|string $modelName): GenerativeModel 92 | { 93 | return new GenerativeModel( 94 | $this, 95 | $modelName, 96 | ); 97 | } 98 | 99 | public function embeddingModel(ModelName|string $modelName): EmbeddingModel 100 | { 101 | return new EmbeddingModel( 102 | $this, 103 | $modelName, 104 | ); 105 | } 106 | 107 | /** 108 | * @throws ClientExceptionInterface 109 | */ 110 | public function generateContent(GenerateContentRequest $request): GenerateContentResponse 111 | { 112 | $response = $this->doRequest($request); 113 | $json = json_decode($response, associative: true); 114 | 115 | return GenerateContentResponse::fromArray($json); 116 | } 117 | 118 | /** 119 | * @param GenerateContentStreamRequest $request 120 | * @param callable(GenerateContentResponse): void $callback 121 | * @param CurlHandle|null $curl 122 | * @throws BadMethodCallException 123 | * @throws RuntimeException 124 | */ 125 | public function generateContentStream( 126 | GenerateContentStreamRequest $request, 127 | callable $callback, 128 | ?CurlHandle $curl = null, 129 | ): void { 130 | if (!extension_loaded('curl')) { 131 | throw new BadMethodCallException('Gemini API requires `curl` extension for streaming responses'); 132 | } 133 | 134 | $parser = new ObjectListParser( 135 | /* @phpstan-ignore-next-line */ 136 | static fn (array $arr) => $callback(GenerateContentResponse::fromArray($arr)), 137 | ); 138 | 139 | $writeFunction = static function (CurlHandle $ch, string $str) use ($request, $parser): int { 140 | $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); 141 | 142 | return $responseCode === 200 143 | ? $parser->consume($str) 144 | : throw new RuntimeException( 145 | sprintf( 146 | 'Gemini API operation failed: operation=%s, status_code=%d, response=%s', 147 | $request->getOperation(), 148 | $responseCode, 149 | $str, 150 | ), 151 | ); 152 | }; 153 | 154 | $ch = $curl ?? curl_init(); 155 | 156 | if ($ch === false) { 157 | throw new RuntimeException('Gemini API cannot initialize streaming content request'); 158 | } 159 | 160 | $headerLines = []; 161 | foreach ($this->getRequestHeaders() as $name => $values) { 162 | foreach ((array) $values as $value) { 163 | $headerLines[] = "{$name}: {$value}"; 164 | } 165 | } 166 | 167 | curl_setopt($ch, CURLOPT_URL, $this->getRequestUrl($request)); 168 | curl_setopt($ch, CURLOPT_POST, true); 169 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request)); 170 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headerLines); 171 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, $writeFunction); 172 | curl_exec($ch); 173 | curl_close($ch); 174 | } 175 | 176 | /** 177 | * @throws ClientExceptionInterface 178 | */ 179 | public function embedContent(EmbedContentRequest $request): EmbedContentResponse 180 | { 181 | $response = $this->doRequest($request); 182 | $json = json_decode($response, associative: true); 183 | 184 | return EmbedContentResponse::fromArray($json); 185 | } 186 | 187 | /** 188 | * @throws ClientExceptionInterface 189 | */ 190 | public function countTokens(CountTokensRequest $request): CountTokensResponse 191 | { 192 | $response = $this->doRequest($request); 193 | $json = json_decode($response, associative: true); 194 | 195 | return CountTokensResponse::fromArray($json); 196 | } 197 | 198 | /** 199 | * @throws ClientExceptionInterface 200 | */ 201 | public function listModels(): ListModelsResponse 202 | { 203 | $request = new ListModelsRequest(); 204 | $response = $this->doRequest($request); 205 | $json = json_decode($response, associative: true); 206 | 207 | return ListModelsResponse::fromArray($json); 208 | } 209 | 210 | public function withBaseUrl(string $baseUrl): self 211 | { 212 | $clone = clone $this; 213 | $clone->baseUrl = $baseUrl; 214 | 215 | return $clone; 216 | } 217 | 218 | public function withV1BetaVersion(): self 219 | { 220 | return $this->withVersion(GeminiClientInterface::API_VERSION_V1_BETA); 221 | } 222 | 223 | public function withVersion(string $version): self 224 | { 225 | $clone = clone $this; 226 | $clone->version = $version; 227 | 228 | return $clone; 229 | } 230 | 231 | /** 232 | * @param array $headers 233 | * @return self 234 | */ 235 | public function withRequestHeaders(array $headers): self 236 | { 237 | $clone = clone $this; 238 | $clone->requestHeaders = []; 239 | 240 | foreach ($headers as $name => $value) { 241 | $clone->requestHeaders[strtolower($name)] = $value; 242 | } 243 | 244 | return $clone; 245 | } 246 | 247 | /** 248 | * @return array 249 | */ 250 | private function getRequestHeaders(): array 251 | { 252 | return $this->requestHeaders + [ 253 | 'content-type' => 'application/json', 254 | self::API_KEY_HEADER_NAME => $this->apiKey, 255 | ]; 256 | } 257 | 258 | private function getRequestUrl(RequestInterface $request): string 259 | { 260 | return sprintf( 261 | '%s/%s/%s', 262 | $this->baseUrl, 263 | $this->version, 264 | $request->getOperation(), 265 | ); 266 | } 267 | 268 | /** 269 | * @throws ClientExceptionInterface 270 | */ 271 | private function doRequest(RequestInterface $request): string 272 | { 273 | if (!isset($this->client, $this->requestFactory, $this->streamFactory)) { 274 | throw new RuntimeException('Missing client or factory for Gemini API operation'); 275 | } 276 | 277 | $httpRequest = $this->requestFactory 278 | ->createRequest( 279 | $request->getHttpMethod(), 280 | $this->getRequestUrl($request), 281 | ); 282 | 283 | foreach ($this->getRequestHeaders() as $name => $value) { 284 | $httpRequest = $httpRequest->withAddedHeader($name, $value); 285 | } 286 | 287 | $payload = $request->getHttpPayload(); 288 | if (!empty($payload)) { 289 | $stream = $this->streamFactory->createStream($payload); 290 | $httpRequest = $httpRequest->withBody($stream); 291 | } 292 | 293 | $response = $this->client->sendRequest($httpRequest); 294 | 295 | if ($response->getStatusCode() !== 200) { 296 | throw new RuntimeException( 297 | sprintf( 298 | 'Gemini API operation failed: operation=%s, status_code=%d, response=%s', 299 | $request->getOperation(), 300 | $response->getStatusCode(), 301 | $response->getBody(), 302 | ), 303 | ); 304 | } 305 | 306 | return (string) $response->getBody(); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/ClientInterface.php: -------------------------------------------------------------------------------- 1 | modelName, 33 | new Content($parts, Role::User), 34 | $this->taskType, 35 | ); 36 | 37 | return $this->client->embedContent($request); 38 | } 39 | 40 | /** 41 | * embedContentWithTitle will override the task type with TaskType::RETRIEVAL_DOCUMENT. 42 | * This is not a persistent change. 43 | * 44 | * @throws ClientExceptionInterface 45 | */ 46 | public function embedContentWithTitle(string $title, PartInterface ...$parts): EmbedContentResponse 47 | { 48 | $request = new EmbedContentRequest( 49 | $this->modelName, 50 | new Content($parts, Role::User), 51 | TaskType::RETRIEVAL_DOCUMENT, 52 | $title, 53 | ); 54 | 55 | return $this->client->embedContent($request); 56 | } 57 | 58 | public function withTaskType(TaskType $taskType): self 59 | { 60 | $clone = clone $this; 61 | $clone->taskType = $taskType; 62 | 63 | return $clone; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Enums/BlockReason.php: -------------------------------------------------------------------------------- 1 | config['candidateCount'] = $candidateCount; 34 | 35 | return $clone; 36 | } 37 | 38 | /** 39 | * @param string[] $stopSequences 40 | * @return $this 41 | */ 42 | public function withStopSequences(array $stopSequences): self 43 | { 44 | $this->ensureArrayOfString($stopSequences); 45 | 46 | $clone = clone $this; 47 | $clone->config['stopSequences'] = $stopSequences; 48 | 49 | return $clone; 50 | } 51 | 52 | public function withMaxOutputTokens(int $maxOutputTokens): self 53 | { 54 | if ($maxOutputTokens < 0) { 55 | throw new UnexpectedValueException('Max output tokens is negative'); 56 | } 57 | 58 | $clone = clone $this; 59 | $clone->config['maxOutputTokens'] = $maxOutputTokens; 60 | 61 | return $clone; 62 | } 63 | 64 | public function withTemperature(float $temperature): self 65 | { 66 | if ($temperature < 0.0 || $temperature > 1.0) { 67 | throw new UnexpectedValueException('Temperature is negative or more than 1'); 68 | } 69 | 70 | $clone = clone $this; 71 | $clone->config['temperature'] = $temperature; 72 | 73 | return $clone; 74 | } 75 | 76 | public function withTopP(float $topP): self 77 | { 78 | if ($topP < 0.0) { 79 | throw new UnexpectedValueException('Top-p is negative'); 80 | } 81 | 82 | $clone = clone $this; 83 | $clone->config['topP'] = $topP; 84 | 85 | return $clone; 86 | } 87 | 88 | public function withTopK(int $topK): self 89 | { 90 | if ($topK < 0) { 91 | throw new UnexpectedValueException('Top-k is negative'); 92 | } 93 | 94 | $clone = clone $this; 95 | $clone->config['topK'] = $topK; 96 | 97 | return $clone; 98 | } 99 | 100 | /** 101 | * @return array{ 102 | * candidateCount?: int, 103 | * stopSequences?: string[], 104 | * maxOutputTokens?: int, 105 | * temperature?: float, 106 | * topP?: float, 107 | * topK?: int, 108 | * } 109 | */ 110 | public function jsonSerialize(): array 111 | { 112 | return $this->config; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/GenerativeModel.php: -------------------------------------------------------------------------------- 1 | generateContentWithContents([$content]); 45 | } 46 | 47 | /** 48 | * @param Content[] $contents 49 | * @throws ClientExceptionInterface 50 | */ 51 | public function generateContentWithContents(array $contents): GenerateContentResponse 52 | { 53 | $this->ensureArrayOfType($contents, Content::class); 54 | 55 | $request = new GenerateContentRequest( 56 | $this->modelName, 57 | $contents, 58 | $this->safetySettings, 59 | $this->generationConfig, 60 | $this->systemInstruction, 61 | ); 62 | 63 | return $this->client->generateContent($request); 64 | } 65 | 66 | /** 67 | * @param callable(GenerateContentResponse): void $callback 68 | * @param PartInterface[] $parts 69 | * @param CurlHandle|null $ch 70 | * @return void 71 | */ 72 | public function generateContentStream( 73 | callable $callback, 74 | array $parts, 75 | ?CurlHandle $ch = null, 76 | ): void { 77 | $this->ensureArrayOfType($parts, PartInterface::class); 78 | 79 | $content = new Content($parts, Role::User); 80 | 81 | $this->generateContentStreamWithContents($callback, [$content], $ch); 82 | } 83 | 84 | /** 85 | * @param callable(GenerateContentResponse): void $callback 86 | * @param Content[] $contents 87 | * @param CurlHandle|null $ch 88 | * @return void 89 | */ 90 | public function generateContentStreamWithContents( 91 | callable $callback, 92 | array $contents, 93 | ?CurlHandle $ch = null, 94 | ): void { 95 | $this->ensureArrayOfType($contents, Content::class); 96 | 97 | $request = new GenerateContentStreamRequest( 98 | $this->modelName, 99 | $contents, 100 | $this->safetySettings, 101 | $this->generationConfig, 102 | $this->systemInstruction, 103 | ); 104 | 105 | $this->client->generateContentStream($request, $callback, $ch); 106 | } 107 | 108 | public function startChat(): ChatSession 109 | { 110 | return new ChatSession($this); 111 | } 112 | 113 | /** 114 | * @throws ClientExceptionInterface 115 | */ 116 | public function countTokens(PartInterface ...$parts): CountTokensResponse 117 | { 118 | $content = new Content($parts, Role::User); 119 | $request = new CountTokensRequest( 120 | $this->modelName, 121 | [$content], 122 | ); 123 | 124 | return $this->client->countTokens($request); 125 | } 126 | 127 | public function withAddedSafetySetting(SafetySetting $safetySetting): self 128 | { 129 | $clone = clone $this; 130 | $clone->safetySettings[] = $safetySetting; 131 | 132 | return $clone; 133 | } 134 | 135 | public function withGenerationConfig(GenerationConfig $generationConfig): self 136 | { 137 | $clone = clone $this; 138 | $clone->generationConfig = $generationConfig; 139 | 140 | return $clone; 141 | } 142 | 143 | public function withSystemInstruction(string $systemInstruction): self 144 | { 145 | $clone = clone $this; 146 | $clone->systemInstruction = Content::text($systemInstruction, Role::User); 147 | 148 | return $clone; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Json/ObjectListParser.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 26 | } 27 | 28 | /** 29 | * @param string $str 30 | * @return int 31 | * @throws RuntimeException 32 | */ 33 | public function consume(string $str): int 34 | { 35 | $offset = 0; 36 | for ($i = 0; $i < strlen($str); $i++) { 37 | if ($this->inEscape) { 38 | $this->inEscape = false; 39 | } elseif ($this->inString) { 40 | if ($str[$i] === '\\') { 41 | $this->inEscape = true; 42 | } elseif ($str[$i] === '"') { 43 | $this->inString = false; 44 | } 45 | } elseif ($str[$i] === '"') { 46 | $this->inString = true; 47 | } elseif ($str[$i] === '{') { 48 | if ($this->depth === 0) { 49 | $offset = $i; 50 | } 51 | $this->depth++; 52 | } elseif ($str[$i] === '}') { 53 | $this->depth--; 54 | if ($this->depth === 0) { 55 | $this->json .= substr($str, $offset, $i - $offset + 1); 56 | $arr = json_decode($this->json, true); 57 | 58 | if (json_last_error() !== JSON_ERROR_NONE) { 59 | throw new RuntimeException('ObjectListParser could not decode the given message'); 60 | } 61 | 62 | ($this->callback)($arr); 63 | $this->json = ''; 64 | $offset = $i + 1; 65 | } 66 | } 67 | } 68 | 69 | $this->json .= substr($str, $offset) ?: ''; 70 | 71 | return strlen($str); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Requests/CountTokensRequest.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($this->contents, Content::class); 29 | } 30 | 31 | public function getOperation(): string 32 | { 33 | return "{$this->modelNameToString($this->modelName)}:countTokens"; 34 | } 35 | 36 | public function getHttpMethod(): string 37 | { 38 | return 'POST'; 39 | } 40 | 41 | public function getHttpPayload(): string 42 | { 43 | return (string) $this; 44 | } 45 | 46 | /** 47 | * @return array{ 48 | * model: string, 49 | * contents: Content[], 50 | * } 51 | */ 52 | public function jsonSerialize(): array 53 | { 54 | return [ 55 | 'model' => $this->modelNameToString($this->modelName), 56 | 'contents' => $this->contents, 57 | ]; 58 | } 59 | 60 | public function __toString(): string 61 | { 62 | return json_encode($this) ?: ''; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Requests/EmbedContentRequest.php: -------------------------------------------------------------------------------- 1 | title) && $this->taskType !== TaskType::RETRIEVAL_DOCUMENT) { 27 | throw new BadMethodCallException('Title is only applicable when TaskType is RETRIEVAL_DOCUMENT'); 28 | } 29 | } 30 | 31 | public function getOperation(): string 32 | { 33 | return "{$this->modelNameToString($this->modelName)}:embedContent"; 34 | } 35 | 36 | public function getHttpMethod(): string 37 | { 38 | return 'POST'; 39 | } 40 | 41 | public function getHttpPayload(): string 42 | { 43 | return (string) $this; 44 | } 45 | 46 | /** 47 | * @return array{ 48 | * content: Content, 49 | * taskType?: TaskType, 50 | * title?: string, 51 | * } 52 | */ 53 | public function jsonSerialize(): array 54 | { 55 | $arr = [ 56 | 'content' => $this->content, 57 | ]; 58 | 59 | if (isset($this->taskType)) { 60 | $arr['taskType'] = $this->taskType; 61 | } 62 | 63 | if (isset($this->title)) { 64 | $arr['title'] = $this->title; 65 | } 66 | 67 | return $arr; 68 | } 69 | 70 | public function __toString(): string 71 | { 72 | return json_encode($this) ?: ''; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Requests/GenerateContentRequest.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($this->contents, Content::class); 37 | $this->ensureArrayOfType($this->safetySettings, SafetySetting::class); 38 | } 39 | 40 | public function getOperation(): string 41 | { 42 | return "{$this->modelNameToString($this->modelName)}:generateContent"; 43 | } 44 | 45 | public function getHttpMethod(): string 46 | { 47 | return 'POST'; 48 | } 49 | 50 | public function getHttpPayload(): string 51 | { 52 | return (string) $this; 53 | } 54 | 55 | /** 56 | * @return array{ 57 | * model: string, 58 | * contents: Content[], 59 | * safetySettings?: SafetySetting[], 60 | * generationConfig?: GenerationConfig, 61 | * systemInstruction?: Content, 62 | * } 63 | */ 64 | public function jsonSerialize(): array 65 | { 66 | $arr = [ 67 | 'model' => $this->modelNameToString($this->modelName), 68 | 'contents' => $this->contents, 69 | ]; 70 | 71 | if (!empty($this->safetySettings)) { 72 | $arr['safetySettings'] = $this->safetySettings; 73 | } 74 | 75 | if ($this->generationConfig) { 76 | $arr['generationConfig'] = $this->generationConfig; 77 | } 78 | 79 | if ($this->systemInstruction) { 80 | $arr['systemInstruction'] = $this->systemInstruction; 81 | } 82 | 83 | return $arr; 84 | } 85 | 86 | public function __toString(): string 87 | { 88 | return json_encode($this) ?: ''; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Requests/GenerateContentStreamRequest.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($this->contents, Content::class); 37 | $this->ensureArrayOfType($this->safetySettings, SafetySetting::class); 38 | } 39 | 40 | public function getOperation(): string 41 | { 42 | return "{$this->modelNameToString($this->modelName)}:streamGenerateContent"; 43 | } 44 | 45 | public function getHttpMethod(): string 46 | { 47 | return 'POST'; 48 | } 49 | 50 | public function getHttpPayload(): string 51 | { 52 | return (string) $this; 53 | } 54 | 55 | /** 56 | * @return array{ 57 | * model: string, 58 | * contents: Content[], 59 | * safetySettings?: SafetySetting[], 60 | * generationConfig?: GenerationConfig, 61 | * systemInstruction?: Content, 62 | * } 63 | */ 64 | public function jsonSerialize(): array 65 | { 66 | $arr = [ 67 | 'model' => $this->modelNameToString($this->modelName), 68 | 'contents' => $this->contents, 69 | ]; 70 | 71 | if (!empty($this->safetySettings)) { 72 | $arr['safetySettings'] = $this->safetySettings; 73 | } 74 | 75 | if ($this->generationConfig) { 76 | $arr['generationConfig'] = $this->generationConfig; 77 | } 78 | 79 | if ($this->systemInstruction) { 80 | $arr['systemInstruction'] = $this->systemInstruction; 81 | } 82 | 83 | return $arr; 84 | } 85 | 86 | public function __toString(): string 87 | { 88 | return json_encode($this) ?: ''; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Requests/ListModelsRequest.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($safetyRatings, SafetyRating::class); 41 | } 42 | 43 | /** 44 | * @param array{ 45 | * citationMetadata: array{citationSources: array}, 46 | * safetyRatings: array, 47 | * content: array{parts: array, role: string}, 48 | * finishReason: string, 49 | * tokenCount: int, 50 | * index: int, 51 | * } $candidate 52 | * @return self 53 | */ 54 | public static function fromArray(array $candidate): self 55 | { 56 | $citationMetadata = isset($candidate['citationMetadata']) 57 | ? CitationMetadata::fromArray($candidate['citationMetadata']) 58 | : new CitationMetadata(); 59 | 60 | $safetyRatings = array_map( 61 | static fn (array $rating): SafetyRating => SafetyRating::fromArray($rating), 62 | $candidate['safetyRatings'] ?? [], 63 | ); 64 | 65 | $content = isset($candidate['content']) 66 | ? Content::fromArray($candidate['content']) 67 | : Content::text('', Role::Model); 68 | 69 | $finishReason = isset($candidate['finishReason']) 70 | ? FinishReason::from($candidate['finishReason']) 71 | : FinishReason::OTHER; 72 | 73 | return new self( 74 | $content, 75 | $finishReason, 76 | $citationMetadata, 77 | $safetyRatings, 78 | $candidate['tokenCount'] ?? 0, 79 | $candidate['index'] ?? 0, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Resources/CitationMetadata.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($citationSources, CitationSource::class); 20 | } 21 | 22 | /** 23 | * @param array{ 24 | * citationSources: array, 25 | * } $array 26 | * @return self 27 | */ 28 | public static function fromArray(array $array): self 29 | { 30 | $citationSources = array_map( 31 | static fn (array $source): CitationSource => CitationSource::fromArray($source), 32 | $array['citationSources'] ?? [], 33 | ); 34 | 35 | return new self($citationSources); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Resources/CitationSource.php: -------------------------------------------------------------------------------- 1 | !is_null($v)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Resources/Content.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($parts, PartInterface::class); 28 | } 29 | 30 | public function addText(string $text): self 31 | { 32 | $this->parts[] = new TextPart($text); 33 | 34 | return $this; 35 | } 36 | 37 | public function addImage(MimeType $mimeType, string $image): self 38 | { 39 | $this->parts[] = new ImagePart($mimeType, $image); 40 | 41 | return $this; 42 | } 43 | 44 | public function addFile(MimeType $mimeType, string $file): self 45 | { 46 | $this->parts[] = new FilePart($mimeType, $file); 47 | 48 | return $this; 49 | } 50 | 51 | public static function text( 52 | string $text, 53 | Role $role = Role::User, 54 | ): self { 55 | return new self( 56 | [ 57 | new TextPart($text), 58 | ], 59 | $role, 60 | ); 61 | } 62 | 63 | public static function image( 64 | MimeType $mimeType, 65 | string $image, 66 | Role $role = Role::User 67 | ): self { 68 | return new self( 69 | [ 70 | new ImagePart($mimeType, $image), 71 | ], 72 | $role, 73 | ); 74 | } 75 | 76 | public static function file( 77 | MimeType $mimeType, 78 | string $file, 79 | Role $role = Role::User 80 | ): self { 81 | return new self( 82 | [ 83 | new FilePart($mimeType, $file), 84 | ], 85 | $role, 86 | ); 87 | } 88 | 89 | public static function textAndImage( 90 | string $text, 91 | MimeType $mimeType, 92 | string $image, 93 | Role $role = Role::User, 94 | ): self { 95 | return new self( 96 | [ 97 | new TextPart($text), 98 | new ImagePart($mimeType, $image), 99 | ], 100 | $role, 101 | ); 102 | } 103 | 104 | public static function textAndFile( 105 | string $text, 106 | MimeType $mimeType, 107 | string $file, 108 | Role $role = Role::User, 109 | ): self { 110 | return new self( 111 | [ 112 | new TextPart($text), 113 | new FilePart($mimeType, $file), 114 | ], 115 | $role, 116 | ); 117 | } 118 | 119 | /** 120 | * @param array{ 121 | * parts: array, 122 | * role: string, 123 | * } $content 124 | * @return self 125 | */ 126 | public static function fromArray(array $content): self 127 | { 128 | $parts = []; 129 | foreach ($content['parts'] as $part) { 130 | if (!empty($part['text'])) { 131 | $parts[] = new TextPart($part['text']); 132 | } 133 | 134 | if (!empty($part['inlineData'])) { 135 | $mimeType = MimeType::from($part['inlineData']['mimeType']); 136 | $parts[] = new FilePart($mimeType, $part['inlineData']['data']); 137 | } 138 | } 139 | 140 | return new self( 141 | $parts, 142 | Role::from($content['role']), 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Resources/ContentEmbedding.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfFloat($this->values); 23 | } 24 | 25 | /** 26 | * @param array{values: float[]} $values 27 | * @return self 28 | */ 29 | public static function fromArray(array $values): self 30 | { 31 | if (!isset($values['values']) || !is_array($values['values'])) { 32 | throw new InvalidArgumentException('The required "values" key is missing or is not an array'); 33 | } 34 | 35 | return new self($values['values']); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Resources/Model.php: -------------------------------------------------------------------------------- 1 | $this->name, 86 | 'version' => $this->version, 87 | 'displayName' => $this->displayName, 88 | 'description' => $this->description, 89 | 'inputTokenLimit' => $this->inputTokenLimit, 90 | 'outputTokenLimit' => $this->outputTokenLimit, 91 | 'supportedGenerationMethods' => $this->supportedGenerationMethods, 92 | ]; 93 | 94 | if ($this->temperature !== null) { 95 | $arr['temperature'] = $this->temperature; 96 | } 97 | 98 | if ($this->topP !== null) { 99 | $arr['topP'] = $this->topP; 100 | } 101 | 102 | if ($this->topK !== null) { 103 | $arr['topK'] = $this->topK; 104 | } 105 | 106 | return $arr; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Resources/ModelName.php: -------------------------------------------------------------------------------- 1 | [ 32 | 'mimeType' => $this->mimeType->value, 33 | 'data' => $this->data, 34 | ], 35 | ]; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | return json_encode($this) ?: ''; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Resources/Parts/ImagePart.php: -------------------------------------------------------------------------------- 1 | $this->text]; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return json_encode($this) ?: ''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Resources/PromptFeedback.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($safetyRatings, SafetyRating::class); 24 | } 25 | 26 | /** 27 | * @param array{ 28 | * blockReason: string|null, 29 | * safetyRatings?: array 30 | * } $array 31 | * @return self 32 | */ 33 | public static function fromArray(array $array): self 34 | { 35 | $blockReason = BlockReason::tryFrom($array['blockReason'] ?? ''); 36 | $safetyRatings = array_map( 37 | static fn (array $rating): SafetyRating => SafetyRating::fromArray($rating), 38 | $array['safetyRatings'] ?? [], 39 | ); 40 | 41 | return new self($blockReason, $safetyRatings); 42 | } 43 | 44 | /** 45 | * @return array> 46 | */ 47 | public function jsonSerialize(): array 48 | { 49 | $arr = []; 50 | 51 | if ($this->blockReason) { 52 | $arr['blockReason'] = $this->blockReason->value; 53 | } 54 | 55 | if (!empty($this->safetyRatings)) { 56 | $arr['safetyRatings'] = $this->safetyRatings; 57 | } 58 | 59 | return $arr; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Resources/SafetyRating.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | public function jsonSerialize(): array 41 | { 42 | $arr = [ 43 | 'category' => $this->category->value, 44 | 'probability' => $this->probability->value, 45 | ]; 46 | 47 | if ($this->blocked !== null) { 48 | $arr['blocked'] = $this->blocked; 49 | } 50 | 51 | return $arr; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Responses/CountTokensResponse.php: -------------------------------------------------------------------------------- 1 | ensureArrayOfType($candidates, Candidate::class); 27 | } 28 | 29 | /** 30 | * @return PartInterface[] 31 | */ 32 | public function parts(): array 33 | { 34 | if (empty($this->candidates)) { 35 | throw new ValueError( 36 | 'The `GenerateContentResponse::parts()` quick accessor '. 37 | 'only works for a single candidate, but none were returned. '. 38 | 'Check the `GenerateContentResponse::$promptFeedback` to see if the prompt was blocked.' 39 | ); 40 | } 41 | 42 | if (count($this->candidates) > 1) { 43 | throw new ValueError( 44 | 'The `GenerateContentResponse::parts()` quick accessor '. 45 | 'only works with a single candidate. '. 46 | 'With multiple candidates use GenerateContentResponse.candidates[index].text' 47 | ); 48 | } 49 | 50 | return $this->candidates[0]->content->parts; 51 | } 52 | 53 | public function text(): string 54 | { 55 | $parts = $this->parts(); 56 | 57 | if (count($parts) > 1 || !$parts[0] instanceof TextPart) { 58 | throw new ValueError( 59 | 'The `GenerateContentResponse::text()` quick accessor '. 60 | 'only works for simple (single-`Part`) text responses. '. 61 | 'This response contains multiple `Parts`. '. 62 | 'Use the `GenerateContentResponse::parts()` accessor '. 63 | 'or the full `GenerateContentResponse.candidates[index].content.parts` lookup instead' 64 | ); 65 | } 66 | 67 | return $parts[0]->text; 68 | } 69 | 70 | /** 71 | * @param array{ 72 | * promptFeedback: array{ 73 | * blockReason: string|null, 74 | * safetyRatings?: array, 75 | * }, 76 | * candidates: array}, 78 | * safetyRatings: array, 79 | * content: array{parts: array, role: string}, 80 | * finishReason: string, 81 | * tokenCount: int, 82 | * index: int 83 | * }>, 84 | * } $array 85 | * @return self 86 | */ 87 | public static function fromArray(array $array): self 88 | { 89 | $promptFeedback = null; 90 | if (!empty($array['promptFeedback'])) { 91 | $promptFeedback = PromptFeedback::fromArray($array['promptFeedback']); 92 | } 93 | 94 | $candidates = array_map( 95 | static fn (array $candidate): Candidate => Candidate::fromArray($candidate), 96 | $array['candidates'] ?? [], 97 | ); 98 | 99 | return new self($candidates, $promptFeedback); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Responses/ListModelsResponse.php: -------------------------------------------------------------------------------- 1 | } $json 32 | * @return self 33 | */ 34 | public static function fromArray(array $json): self 35 | { 36 | $models = array_map( 37 | static fn (array $arr): Model => Model::fromArray($arr), 38 | $json['models'], 39 | ); 40 | 41 | return new self($models); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SafetySetting.php: -------------------------------------------------------------------------------- 1 | $this->category->value, 31 | 'threshold' => $this->threshold->value, 32 | ]; 33 | } 34 | 35 | public function __toString() 36 | { 37 | return json_encode($this) ?: ''; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Traits/ArrayTypeValidator.php: -------------------------------------------------------------------------------- 1 | $items 19 | * @param class-string $classString 20 | * @return void 21 | * @throws InvalidArgumentException 22 | */ 23 | private function ensureArrayOfType(array $items, string $classString): void 24 | { 25 | foreach ($items as $item) { 26 | if (!$item instanceof $classString) { 27 | throw new InvalidArgumentException( 28 | sprintf( 29 | 'Expected type %s but found %s', 30 | $classString, 31 | is_object($item) ? $item::class : gettype($item), 32 | ), 33 | ); 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * @param array $items 40 | * @return void 41 | * @throws InvalidArgumentException 42 | */ 43 | private function ensureArrayOfString(array $items): void 44 | { 45 | foreach ($items as $item) { 46 | if (!is_string($item)) { 47 | throw new InvalidArgumentException( 48 | sprintf( 49 | 'Expected string but found %s', 50 | is_object($item) ? $item::class : gettype($item), 51 | ), 52 | ); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * @param array $items 59 | * @return void 60 | * @throws InvalidArgumentException 61 | */ 62 | private function ensureArrayOfFloat(array $items): void 63 | { 64 | foreach ($items as $item) { 65 | if (!is_float($item)) { 66 | throw new InvalidArgumentException( 67 | sprintf( 68 | 'Expected float but found %s', 69 | is_object($item) ? $item::class : gettype($item), 70 | ), 71 | ); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Traits/ModelNameToString.php: -------------------------------------------------------------------------------- 1 | value; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Unit/ClientTest.php: -------------------------------------------------------------------------------- 1 | createMock(HttpClientInterface::class), 31 | ); 32 | self::assertInstanceOf(Client::class, $client); 33 | } 34 | 35 | public function testWithBaseUrl(): void 36 | { 37 | $client = new Client( 38 | 'test-api-key', 39 | $this->createMock(HttpClientInterface::class), 40 | ); 41 | $client = $client->withBaseUrl('test-base-url'); 42 | self::assertInstanceOf(Client::class, $client); 43 | } 44 | 45 | public function testGeminiPro(): void 46 | { 47 | $client = new Client( 48 | 'test-api-key', 49 | $this->createMock(HttpClientInterface::class), 50 | ); 51 | $model = $client->generativeModel(ModelName::GEMINI_PRO); 52 | self::assertInstanceOf(GenerativeModel::class, $model); 53 | self::assertEquals(ModelName::GEMINI_PRO, $model->modelName); 54 | } 55 | 56 | public function testGeminiProWithEnum(): void 57 | { 58 | $client = new Client( 59 | 'test-api-key', 60 | $this->createMock(HttpClientInterface::class), 61 | ); 62 | $model = $client->generativeModel(ModelNameEnum::GeminiPro); 63 | self::assertInstanceOf(GenerativeModel::class, $model); 64 | self::assertEquals(ModelNameEnum::GeminiPro, $model->modelName); 65 | } 66 | 67 | public function testGenerativeModel() 68 | { 69 | $client = new Client( 70 | 'test-api-key', 71 | $this->createMock(HttpClientInterface::class), 72 | ); 73 | $model = $client->generativeModel(ModelName::EMBEDDING_001); 74 | self::assertInstanceOf(GenerativeModel::class, $model); 75 | self::assertEquals(ModelName::EMBEDDING_001, $model->modelName); 76 | } 77 | 78 | public function testGenerateContent(): void 79 | { 80 | $httpRequest = new Request( 81 | 'POST', 82 | 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent', 83 | ); 84 | $httpResponse = new Response( 85 | body: <<createMock(RequestFactoryInterface::class); 119 | $requestFactory->expects(self::once()) 120 | ->method('createRequest') 121 | ->with('POST', 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent') 122 | ->willReturn($httpRequest); 123 | 124 | $httpRequest = $httpRequest 125 | ->withAddedHeader(GeminiAPIClientInterface::API_KEY_HEADER_NAME, 'test-api-key') 126 | ->withAddedHeader('content-type', 'application/json'); 127 | 128 | $stream = Utils::streamFor('{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"this is a text"}],"role":"user"}]}'); 129 | $streamFactory = $this->createMock(StreamFactoryInterface::class); 130 | $streamFactory->expects(self::once()) 131 | ->method('createStream') 132 | ->with('{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"this is a text"}],"role":"user"}]}') 133 | ->willReturn($stream); 134 | 135 | $httpClient = $this->createMock(HttpClientInterface::class); 136 | $httpClient->expects(self::once()) 137 | ->method('sendRequest') 138 | ->with($httpRequest->withBody($stream)) 139 | ->willReturn($httpResponse); 140 | 141 | $client = new Client( 142 | 'test-api-key', 143 | $httpClient, 144 | $requestFactory, 145 | $streamFactory, 146 | ); 147 | $request = new GenerateContentRequest( 148 | ModelName::GEMINI_PRO, 149 | [Content::text('this is a text')], 150 | ); 151 | $response = $client->generateContent($request); 152 | self::assertEquals('This is the Gemini Pro response', $response->text()); 153 | } 154 | 155 | public function testEmbedContent(): void 156 | { 157 | $httpRequest = new Request( 158 | 'POST', 159 | 'https://generativelanguage.googleapis.com/v1/models/embedding-001:embedContent', 160 | ); 161 | $httpResponse = new Response( 162 | body: <<createMock(RequestFactoryInterface::class); 174 | $requestFactory->expects(self::once()) 175 | ->method('createRequest') 176 | ->with('POST', 'https://generativelanguage.googleapis.com/v1/models/embedding-001:embedContent') 177 | ->willReturn($httpRequest); 178 | 179 | $httpRequest = $httpRequest 180 | ->withAddedHeader(GeminiAPIClientInterface::API_KEY_HEADER_NAME, 'test-api-key') 181 | ->withAddedHeader('content-type', 'application/json'); 182 | 183 | $stream = Utils::streamFor('{"content":{"parts":[{"text":"this is a text"}],"role":"user"}}'); 184 | $streamFactory = $this->createMock(StreamFactoryInterface::class); 185 | $streamFactory->expects(self::once()) 186 | ->method('createStream') 187 | ->with('{"content":{"parts":[{"text":"this is a text"}],"role":"user"}}') 188 | ->willReturn($stream); 189 | 190 | $httpClient = $this->createMock(HttpClientInterface::class); 191 | $httpClient->expects(self::once()) 192 | ->method('sendRequest') 193 | ->with($httpRequest->withBody($stream)) 194 | ->willReturn($httpResponse); 195 | 196 | $client = new Client( 197 | 'test-api-key', 198 | $httpClient, 199 | $requestFactory, 200 | $streamFactory, 201 | ); 202 | $request = new EmbedContentRequest( 203 | ModelName::EMBEDDING_001, 204 | Content::text('this is a text'), 205 | ); 206 | $response = $client->embedContent($request); 207 | self::assertEquals([0.041395925, -0.017692696], $response->embedding->values); 208 | } 209 | 210 | public function testCountTokens(): void 211 | { 212 | $httpRequest = new Request( 213 | 'POST', 214 | 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:countTokens', 215 | ); 216 | $httpResponse = new Response( 217 | body: <<createMock(RequestFactoryInterface::class); 224 | $requestFactory->expects(self::once()) 225 | ->method('createRequest') 226 | ->with('POST', 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:countTokens') 227 | ->willReturn($httpRequest); 228 | 229 | $httpRequest = $httpRequest 230 | ->withAddedHeader(GeminiAPIClientInterface::API_KEY_HEADER_NAME, 'test-api-key') 231 | ->withAddedHeader('content-type', 'application/json'); 232 | 233 | $stream = Utils::streamFor('{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"this is a text"}],"role":"user"}]}'); 234 | $streamFactory = $this->createMock(StreamFactoryInterface::class); 235 | $streamFactory->expects(self::once()) 236 | ->method('createStream') 237 | ->with('{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"this is a text"}],"role":"user"}]}') 238 | ->willReturn($stream); 239 | 240 | $httpClient = $this->createMock(HttpClientInterface::class); 241 | $httpClient->expects(self::once()) 242 | ->method('sendRequest') 243 | ->with($httpRequest->withBody($stream)) 244 | ->willReturn($httpResponse); 245 | 246 | $client = new Client( 247 | 'test-api-key', 248 | $httpClient, 249 | $requestFactory, 250 | $streamFactory, 251 | ); 252 | $request = new CountTokensRequest( 253 | ModelName::GEMINI_PRO, 254 | [Content::text('this is a text')], 255 | ); 256 | $response = $client->countTokens($request); 257 | self::assertEquals(10, $response->totalTokens); 258 | } 259 | 260 | public function testListModels(): void 261 | { 262 | $httpRequest = new Request( 263 | 'GET', 264 | 'https://generativelanguage.googleapis.com/v1/models', 265 | ); 266 | $httpResponse = new Response( 267 | body: <<createMock(RequestFactoryInterface::class); 305 | $requestFactory->expects(self::once()) 306 | ->method('createRequest') 307 | ->with('GET', 'https://generativelanguage.googleapis.com/v1/models') 308 | ->willReturn($httpRequest); 309 | 310 | $httpRequest = $httpRequest 311 | ->withAddedHeader(GeminiAPIClientInterface::API_KEY_HEADER_NAME, 'test-api-key') 312 | ->withAddedHeader('content-type', 'application/json'); 313 | 314 | $httpClient = $this->createMock(HttpClientInterface::class); 315 | $httpClient->expects(self::once()) 316 | ->method('sendRequest') 317 | ->with($httpRequest) 318 | ->willReturn($httpResponse); 319 | 320 | $client = new Client( 321 | 'test-api-key', 322 | $httpClient, 323 | $requestFactory, 324 | ); 325 | $response = $client->listModels(); 326 | self::assertCount(2, $response->models); 327 | self::assertEquals('models/gemini-pro', $response->models[0]->name); 328 | self::assertEquals('models/gemini-pro-vision', $response->models[1]->name); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /tests/Unit/Requests/CountTokensRequestTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 39 | 40 | new CountTokensRequest( 41 | ModelName::GEMINI_PRO, 42 | // @phpstan-ignore-next-line 43 | [ 44 | new Content([], Role::User), 45 | new TextPart('This is a text'), 46 | ], 47 | ); 48 | } 49 | 50 | public function testGetOperation(): void 51 | { 52 | $request = new CountTokensRequest(ModelName::GEMINI_PRO, []); 53 | self::assertEquals('models/gemini-pro:countTokens', $request->getOperation()); 54 | } 55 | 56 | public function testGetHttpMethod(): void 57 | { 58 | $request = new CountTokensRequest(ModelName::GEMINI_PRO, []); 59 | self::assertEquals('POST', $request->getHttpMethod()); 60 | } 61 | 62 | public function testGetHttpPayload(): void 63 | { 64 | $request = new CountTokensRequest( 65 | ModelName::GEMINI_PRO, 66 | [ 67 | new Content([new TextPart('This is a text')], Role::User), 68 | ], 69 | ); 70 | 71 | $expected = '{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"This is a text"}],"role":"user"}]}'; 72 | self::assertEquals($expected, $request->getHttpPayload()); 73 | } 74 | 75 | public function testJsonSerialize(): void 76 | { 77 | $request = new CountTokensRequest( 78 | ModelName::GEMINI_PRO, 79 | [ 80 | new Content([new TextPart('This is a text')], Role::User), 81 | ], 82 | ); 83 | 84 | $expected = [ 85 | 'model' => 'models/gemini-pro', 86 | 'contents' => [ 87 | new Content([new TextPart('This is a text')], Role::User), 88 | ], 89 | ]; 90 | self::assertEquals($expected, $request->jsonSerialize()); 91 | } 92 | 93 | public function test__toString(): void 94 | { 95 | $request = new CountTokensRequest( 96 | ModelName::GEMINI_PRO, 97 | [ 98 | new Content( 99 | [new TextPart('This is a text')], 100 | Role::User, 101 | ) 102 | ], 103 | ); 104 | 105 | $expected = '{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"This is a text"}],"role":"user"}]}'; 106 | self::assertEquals($expected, (string) $request); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Unit/Requests/EmbedContentRequestTest.php: -------------------------------------------------------------------------------- 1 | expectException(BadMethodCallException::class); 49 | $this->expectExceptionMessage('Title is only applicable when TaskType is RETRIEVAL_DOCUMENT'); 50 | 51 | new EmbedContentRequest( 52 | ModelName::EMBEDDING_001, 53 | Content::text('this is a test'), 54 | TaskType::RETRIEVAL_QUERY, 55 | 'this is a title', 56 | ); 57 | } 58 | 59 | public function testGetHttpPayload(): void 60 | { 61 | $request = new EmbedContentRequest( 62 | ModelName::EMBEDDING_001, 63 | Content::text('this is a test'), 64 | ); 65 | self::assertEquals('{"content":{"parts":[{"text":"this is a test"}],"role":"user"}}', $request->getHttpPayload()); 66 | } 67 | 68 | public function testGetHttpMethod(): void 69 | { 70 | $request = new EmbedContentRequest( 71 | ModelName::EMBEDDING_001, 72 | Content::text('this is a test'), 73 | ); 74 | self::assertEquals('POST', $request->getHttpMethod()); 75 | } 76 | 77 | public function testGetOperation(): void 78 | { 79 | $request = new EmbedContentRequest( 80 | ModelName::EMBEDDING_001, 81 | Content::text('this is a test'), 82 | ); 83 | self::assertEquals('models/embedding-001:embedContent', $request->getOperation()); 84 | } 85 | 86 | public function testJsonSerialize(): void 87 | { 88 | $request = new EmbedContentRequest( 89 | ModelName::EMBEDDING_001, 90 | $content = Content::text('this is a test'), 91 | TaskType::RETRIEVAL_DOCUMENT, 92 | 'this is a title', 93 | ); 94 | $expected = [ 95 | 'content' => $content, 96 | 'taskType' => TaskType::RETRIEVAL_DOCUMENT, 97 | 'title' => 'this is a title', 98 | ]; 99 | self::assertEquals($expected, $request->jsonSerialize()); 100 | } 101 | 102 | public function test__toString(): void 103 | { 104 | $request = new EmbedContentRequest( 105 | ModelName::EMBEDDING_001, 106 | Content::text('this is a test'), 107 | ); 108 | self::assertEquals('{"content":{"parts":[{"text":"this is a test"}],"role":"user"}}', (string) $request); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Unit/Requests/GenerateContentRequestTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 51 | 52 | new GenerateContentRequest( 53 | ModelName::GEMINI_PRO, 54 | [ 55 | new Content([], Role::User), 56 | new TextPart('This is a text'), 57 | ], 58 | [], 59 | null, 60 | ); 61 | } 62 | 63 | public function testConstructorWithSafetySettings(): void 64 | { 65 | $request = new GenerateContentRequest( 66 | ModelName::GEMINI_PRO, 67 | [], 68 | [ 69 | new SafetySetting( 70 | HarmCategory::HARM_CATEGORY_HATE_SPEECH, 71 | HarmBlockThreshold::BLOCK_LOW_AND_ABOVE, 72 | ), 73 | new SafetySetting( 74 | HarmCategory::HARM_CATEGORY_MEDICAL, 75 | HarmBlockThreshold::BLOCK_MEDIUM_AND_ABOVE, 76 | ), 77 | ], 78 | null, 79 | ); 80 | self::assertInstanceOf(GenerateContentRequest::class, $request); 81 | } 82 | 83 | public function testConstructorWithInvalidSafetySettings(): void 84 | { 85 | $this->expectException(InvalidArgumentException::class); 86 | 87 | new GenerateContentRequest( 88 | ModelName::GEMINI_PRO, 89 | [], 90 | [ 91 | new SafetySetting( 92 | HarmCategory::HARM_CATEGORY_UNSPECIFIED, 93 | HarmBlockThreshold::HARM_BLOCK_THRESHOLD_UNSPECIFIED, 94 | ), 95 | new SafetyRating( 96 | HarmCategory::HARM_CATEGORY_UNSPECIFIED, 97 | HarmProbability::HARM_PROBABILITY_UNSPECIFIED, 98 | null, 99 | ) 100 | ], 101 | null, 102 | ); 103 | } 104 | 105 | public function testConstructorWithGenerationConfig(): void 106 | { 107 | $request = new GenerateContentRequest( 108 | ModelName::GEMINI_PRO, 109 | [], 110 | [], 111 | new GenerationConfig(), 112 | ); 113 | self::assertInstanceOf(GenerateContentRequest::class, $request); 114 | } 115 | 116 | public function testGetOperation(): void 117 | { 118 | $request = new GenerateContentRequest(ModelName::GEMINI_PRO, []); 119 | self::assertEquals('models/gemini-pro:generateContent', $request->getOperation()); 120 | } 121 | 122 | public function testGetHttpMethod(): void 123 | { 124 | $request = new GenerateContentRequest(ModelName::GEMINI_PRO, []); 125 | self::assertEquals('POST', $request->getHttpMethod()); 126 | } 127 | 128 | public function testGetHttpPayload(): void 129 | { 130 | $request = new GenerateContentRequest( 131 | ModelName::GEMINI_PRO, 132 | [ 133 | new Content([new TextPart('This is a text')], Role::User), 134 | ], 135 | ); 136 | $expected = '{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"This is a text"}],"role":"user"}]}'; 137 | self::assertEquals($expected, $request->getHttpPayload()); 138 | } 139 | 140 | public function testJsonSerialize(): void 141 | { 142 | $request = new GenerateContentRequest( 143 | ModelName::GEMINI_PRO, 144 | [ 145 | new Content([new TextPart('This is a text')], Role::User), 146 | ], 147 | ); 148 | 149 | $expected = [ 150 | 'model' => 'models/gemini-pro', 151 | 'contents' => [ 152 | new Content([new TextPart('This is a text')], Role::User), 153 | ], 154 | ]; 155 | self::assertEquals($expected, $request->jsonSerialize()); 156 | } 157 | 158 | public function test__toString(): void 159 | { 160 | $request = new GenerateContentRequest( 161 | ModelName::GEMINI_PRO, 162 | [ 163 | new Content( 164 | [new TextPart('This is a text')], 165 | Role::User, 166 | ) 167 | ], 168 | ); 169 | 170 | $expected = '{"model":"models\/gemini-pro","contents":[{"parts":[{"text":"This is a text"}],"role":"user"}]}'; 171 | self::assertEquals($expected, (string) $request); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/Unit/Requests/ListModelsRequestTest.php: -------------------------------------------------------------------------------- 1 | getOperation()); 16 | } 17 | 18 | public function testGetHttpMethod() 19 | { 20 | $request = new ListModelsRequest(); 21 | self::assertEquals('GET', $request->getHttpMethod()); 22 | } 23 | 24 | public function testGetHttpPayload() 25 | { 26 | $request = new ListModelsRequest(); 27 | self::assertEmpty($request->getHttpPayload()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/Resources/CandidateTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 62 | 63 | new Candidate( 64 | new Content([], Role::User), 65 | FinishReason::OTHER, 66 | new CitationMetadata(), 67 | [ 68 | new SafetyRating( 69 | HarmCategory::HARM_CATEGORY_MEDICAL, 70 | HarmProbability::HIGH, 71 | false, 72 | ), 73 | new SafetySetting( 74 | HarmCategory::HARM_CATEGORY_DANGEROUS_CONTENT, 75 | HarmBlockThreshold::BLOCK_LOW_AND_ABOVE, 76 | ), 77 | ], 78 | 1, 79 | 1, 80 | ); 81 | } 82 | 83 | public function testFromArray() 84 | { 85 | $candidate = Candidate::fromArray([ 86 | 'content' => ['parts' => [], 'role' => 'user'], 87 | 'safetyRatings' => [], 88 | 'citationMetadata' => [], 89 | 'index' => 1, 90 | 'tokenCount' => 1, 91 | 'finishReason' => 'OTHER', 92 | ]); 93 | 94 | self::assertInstanceOf(Candidate::class, $candidate); 95 | } 96 | 97 | public function testFromArrayWithoutContent() 98 | { 99 | $candidate = Candidate::fromArray([ 100 | 'safetyRatings' => [], 101 | 'citationMetadata' => [], 102 | 'index' => 1, 103 | 'tokenCount' => 1, 104 | 'finishReason' => 'OTHER', 105 | ]); 106 | 107 | self::assertInstanceOf(Candidate::class, $candidate); 108 | } 109 | 110 | public function testFromArrayWithoutFinishReason() 111 | { 112 | $candidate = Candidate::fromArray([ 113 | 'content' => ['parts' => [], 'role' => 'user'], 114 | 'safetyRatings' => [], 115 | 'citationMetadata' => [], 116 | 'index' => 1, 117 | 'tokenCount' => 1, 118 | ]); 119 | 120 | self::assertInstanceOf(Candidate::class, $candidate); 121 | self::assertEquals(FinishReason::OTHER, $candidate->finishReason); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Unit/Resources/CitationMetadataTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 34 | 35 | new CitationMetadata( 36 | [ 37 | new CitationSource(null, null, null, null), 38 | [null, null, null, null], 39 | ], 40 | ); 41 | } 42 | 43 | public function testFromArrayWithNoSources() 44 | { 45 | $citationMetadata = CitationMetadata::fromArray([ 46 | 'citationSources' => [], 47 | ]); 48 | self::assertInstanceOf(CitationMetadata::class, $citationMetadata); 49 | } 50 | 51 | public function testFromArrayWithSources() 52 | { 53 | $citationMetadata = CitationMetadata::fromArray([ 54 | 'citationSources' => [ 55 | [ 56 | 'startIndex' => 1, 57 | 'endIndex' => 49, 58 | ], 59 | [ 60 | 'startIndex' => 50, 61 | 'endIndex' => 99, 62 | ], 63 | ], 64 | ]); 65 | self::assertInstanceOf(CitationMetadata::class, $citationMetadata); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Unit/Resources/CitationSourceTest.php: -------------------------------------------------------------------------------- 1 | startIndex); 22 | self::assertEquals(49, $citationSource->endIndex); 23 | self::assertEquals('test-uri', $citationSource->uri); 24 | self::assertEquals('test-license', $citationSource->license); 25 | } 26 | 27 | public function testFromArray() 28 | { 29 | $citationSource = CitationSource::fromArray([ 30 | 'startIndex' => 1, 31 | 'endIndex' => 49, 32 | 'uri' => 'test-uri', 33 | 'license' => 'test-license', 34 | ]); 35 | self::assertInstanceOf(CitationSource::class, $citationSource); 36 | self::assertEquals(1, $citationSource->startIndex); 37 | self::assertEquals(49, $citationSource->endIndex); 38 | self::assertEquals('test-uri', $citationSource->uri); 39 | self::assertEquals('test-license', $citationSource->license); 40 | } 41 | 42 | public function testJsonSerialize() 43 | { 44 | $citationSource = new CitationSource( 45 | 1, 46 | 49, 47 | 'test-uri', 48 | 'test-license', 49 | ); 50 | $expected = [ 51 | 'startIndex' => 1, 52 | 'endIndex' => 49, 53 | 'uri' => 'test-uri', 54 | 'license' => 'test-license', 55 | ]; 56 | self::assertEquals($expected, $citationSource->jsonSerialize()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Resources/ContentEmbeddingTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 28 | $this->expectExceptionMessage('Expected float but found string'); 29 | 30 | new ContentEmbedding([0.0, 1.0, '2.0']); 31 | } 32 | 33 | public function testFromArrayWithEmptyArray() 34 | { 35 | $embedding = ContentEmbedding::fromArray([ 36 | 'values' => [], 37 | ]); 38 | self::assertInstanceOf(ContentEmbedding::class, $embedding); 39 | } 40 | 41 | public function testFromArrayWithFloatArray() 42 | { 43 | $embedding = ContentEmbedding::fromArray([ 44 | 'values' => [0.0, 1.0], 45 | ]); 46 | self::assertInstanceOf(ContentEmbedding::class, $embedding); 47 | } 48 | 49 | public function testFromArrayWithMissingKey() 50 | { 51 | $this->expectException(InvalidArgumentException::class); 52 | $this->expectExceptionMessage('The required "values" key is missing or is not an array'); 53 | 54 | ContentEmbedding::fromArray([ 55 | 'foo' => [0.0, 1.0], 56 | ]); 57 | } 58 | 59 | public function testFromArrayWithInvalidArray() 60 | { 61 | $this->expectException(InvalidArgumentException::class); 62 | $this->expectExceptionMessage('Expected float but found string'); 63 | 64 | ContentEmbedding::fromArray([ 65 | 'values' => [0.0, 1.0, '2.0'], 66 | ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/Resources/ContentTest.php: -------------------------------------------------------------------------------- 1 | parts); 22 | self::assertEquals(Role::User, $content->role); 23 | } 24 | 25 | public function testConstructorWithContents() 26 | { 27 | $content = new Content( 28 | [new TextPart('this is a text')], 29 | Role::User, 30 | ); 31 | self::assertInstanceOf(Content::class, $content); 32 | self::assertEquals([new TextPart('this is a text')], $content->parts); 33 | self::assertEquals(Role::User, $content->role); 34 | } 35 | 36 | public function testText() 37 | { 38 | $content = Content::text('this is a text', Role::Model); 39 | self::assertInstanceOf(Content::class, $content); 40 | self::assertEquals([new TextPart('this is a text')], $content->parts); 41 | self::assertEquals(Role::Model, $content->role); 42 | } 43 | 44 | public function testImage() 45 | { 46 | $content = Content::image( 47 | MimeType::IMAGE_JPEG, 48 | 'this is an image', 49 | Role::Model, 50 | ); 51 | self::assertInstanceOf(Content::class, $content); 52 | self::assertEquals([new ImagePart(MimeType::IMAGE_JPEG, 'this is an image')], $content->parts); 53 | self::assertEquals(Role::Model, $content->role); 54 | } 55 | 56 | public function testTextAndImage() 57 | { 58 | $content = Content::textAndImage( 59 | 'this is a text', 60 | MimeType::IMAGE_JPEG, 61 | 'this is an image', 62 | Role::Model, 63 | ); 64 | $parts = [ 65 | new TextPart('this is a text'), 66 | new ImagePart(MimeType::IMAGE_JPEG, 'this is an image'), 67 | ]; 68 | self::assertInstanceOf(Content::class, $content); 69 | self::assertEquals($parts, $content->parts); 70 | self::assertEquals(Role::Model, $content->role); 71 | } 72 | 73 | public function testAddText() 74 | { 75 | $content = new Content([], Role::User); 76 | $content->addText('this is a text'); 77 | self::assertEquals([new TextPart('this is a text')], $content->parts); 78 | } 79 | 80 | public function testAddImage() 81 | { 82 | $content = new Content([], Role::User); 83 | $content->addImage(MimeType::IMAGE_JPEG, 'this is an image'); 84 | self::assertEquals([new ImagePart(MimeType::IMAGE_JPEG, 'this is an image')], $content->parts); 85 | } 86 | 87 | public function testFromArrayWithNoParts() 88 | { 89 | $content = Content::fromArray([ 90 | 'parts' => [], 91 | 'role' => 'user', 92 | ]); 93 | self::assertInstanceOf(Content::class, $content); 94 | self::assertEmpty($content->parts); 95 | self::assertEquals(Role::User, $content->role); 96 | } 97 | 98 | public function testFromArrayWithParts() 99 | { 100 | $content = Content::fromArray([ 101 | 'parts' => [ 102 | ['text' => 'this is a text'], 103 | ['inlineData' => ['mimeType' => 'image/jpeg', 'data' => 'this is an image']], 104 | ], 105 | 'role' => 'user', 106 | ]); 107 | $parts = [ 108 | new TextPart('this is a text'), 109 | new FilePart(MimeType::IMAGE_JPEG, 'this is an image'), 110 | ]; 111 | self::assertInstanceOf(Content::class, $content); 112 | self::assertEquals($parts, $content->parts); 113 | self::assertEquals(Role::User, $content->role); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/Unit/Resources/Parts/ImagePartTest.php: -------------------------------------------------------------------------------- 1 | [ 24 | 'mimeType' => 'image/jpeg', 25 | 'data' => 'this is an image', 26 | ], 27 | ]; 28 | self::assertEquals($expected, $part->jsonSerialize()); 29 | } 30 | 31 | public function test__toString() 32 | { 33 | $part = new ImagePart(MimeType::IMAGE_JPEG, 'this is an image'); 34 | $expected = '{"inlineData":{"mimeType":"image\/jpeg","data":"this is an image"}}'; 35 | self::assertEquals($expected, (string) $part); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Unit/Resources/Parts/TextPartTest.php: -------------------------------------------------------------------------------- 1 | 'this is a text'], $part->jsonSerialize()); 22 | } 23 | 24 | public function test__toString() 25 | { 26 | $part = new TextPart('this is a text'); 27 | $expected = '{"text":"this is a text"}'; 28 | self::assertEquals($expected, (string) $part); 29 | } 30 | } 31 | --------------------------------------------------------------------------------