├── .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 |
3 |
4 |
5 |
6 |
7 |
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 |
--------------------------------------------------------------------------------