Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These are supported funding model platforms

github: [aydinfatih,Plytas]
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ $result = $client
$result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table.
```

#### Image Generation
Generate images from text prompts using the Imagen model.

```php
use Gemini\Data\ImageConfig;
use Gemini\Data\GenerationConfig;

$imageConfig = new ImageConfig(aspectRatio: '16:9');
$generationConfig = new GenerationConfig(imageConfig: $imageConfig);

$response = $client->generativeModel(model: 'gemini-2.5-flash-image')
->withGenerationConfig($generationConfig)
->generateContent('Draw a futuristic city');

// Save the image
file_put_contents('image.png', base64_decode($response->parts()[0]->inlineData->data));
```

#### Multi-turn Conversations (Chat)
Using Gemini, you can build freeform conversations across multiple turns.

Expand All @@ -249,6 +267,20 @@ $response = $chat->sendMessage('Rewrite the same story in 1600s England');
echo $response->text(); // In the heart of England's lush countryside, amidst emerald fields and thatched-roof cottages, the village of Willowbrook unfolded a tapestry of love, mystery, and the enchantment of ordinary days in the 17th century.
```

#### Chat with Streaming
You can also stream the response in a chat session. The history is automatically updated with the full response after the stream completes.

```php
$chat = $client->generativeModel(model: 'gemini-2.0-flash')->startChat();

$stream = $chat->streamSendMessage('Hello');

foreach ($stream as $response) {
echo $response->text();
}
```


#### Stream Generate Content
By default, the model returns a response after completing the entire generation process. You can achieve faster interactions by not waiting for the entire result, and instead use streaming to handle partial results.

Expand Down Expand Up @@ -343,7 +375,8 @@ function handleFunctionCall(FunctionCall $functionCall): Content
functionResponse: new FunctionResponse(
name: 'addition',
response: ['answer' => $functionCall->args['number1'] + $functionCall->args['number2']],
)
),
thoughtSignature: 'some-signature' // Optional: Required for some models (e.g. Gemini 3 Pro)
)
],
role: Role::USER
Expand Down Expand Up @@ -382,6 +415,7 @@ $chat = $client
$response = $chat->sendMessage('What is 4 + 3?');

if ($response->parts()[0]->functionCall !== null) {
$thoughtSignature = $response->parts()[0]->thoughtSignature; // Access the thought signature
$functionResponse = handleFunctionCall($response->parts()[0]->functionCall);

$response = $chat->sendMessage($functionResponse);
Expand All @@ -390,6 +424,7 @@ if ($response->parts()[0]->functionCall !== null) {
echo $response->text(); // 4 + 3 = 7
```


#### Speech generation
Gemini allows generating [speech from a text](https://ai.google.dev/gemini-api/docs/speech-generation). To use that, make sure to use a model that supports this functionality. The model will output base64 encoded audio string.

Expand Down
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"natural",
"language",
"processing",
"NLP"
"NLP",
"banana",
"google"
],
"authors": [
{
Expand All @@ -28,10 +30,10 @@
"require-dev": {
"guzzlehttp/guzzle": "^7.8.1",
"guzzlehttp/psr7": "^2.6.2",
"pestphp/pest": "^2.30",
"pestphp/pest": "^2.32.3",
"laravel/pint": "^1.18.1",
"mockery/mockery": "^1.6.7",
"phpstan/phpstan": "^1.10"
"phpstan/phpstan": "^2.1.32"
},
"suggest": {
"ext-fileinfo": "Reads upload file size and mime type if not provided"
Expand Down
10 changes: 9 additions & 1 deletion src/Contracts/Resources/ChatSessionContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@

use Gemini\Data\Blob;
use Gemini\Data\Content;
use Gemini\Data\UploadedFile;
use Gemini\Responses\GenerativeModel\GenerateContentResponse;
use Generator;

interface ChatSessionContract
{
/**
* @param string|Blob|array<string|Blob>|Content ...$parts
*/
public function sendMessage(string|Blob|array|Content ...$parts): GenerateContentResponse;
public function sendMessage(string|Blob|array|Content|UploadedFile ...$parts): GenerateContentResponse;

/**
* @param string|Blob|array<string|Blob|UploadedFile>|Content|UploadedFile ...$parts
* @return \Generator<int, GenerateContentResponse>
*/
public function streamSendMessage(string|Blob|array|Content|UploadedFile ...$parts): Generator;

/**
* @param array<Content> $history
Expand Down
15 changes: 15 additions & 0 deletions src/Enums/BlockReason.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
*/
enum BlockReason: string
{
/**
* The blocked reason is unspecified.
*/
case BLOCKED_REASON_UNSPECIFIED = 'BLOCKED_REASON_UNSPECIFIED';

/**
* Default value. This value is unused.
*/
Expand Down Expand Up @@ -40,4 +45,14 @@ enum BlockReason: string
* Candidates blocked due to unsafe image generation content.
*/
case IMAGE_SAFETY = 'IMAGE_SAFETY';

/**
* The prompt was blocked by Model Armor.
*/
case MODEL_ARMOR = 'MODEL_ARMOR';

/**
* The prompt was blocked as a jailbreak attempt.
*/
case JAILBREAK = 'JAILBREAK';
}
5 changes: 5 additions & 0 deletions src/Enums/FinishReason.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,9 @@ enum FinishReason: string
* Model called too many tools consecutively, thus the system exited execution.
*/
case TOO_MANY_TOOL_CALLS = 'TOO_MANY_TOOL_CALLS';

/**
* The model response was blocked by Model Armor.
*/
case MODEL_ARMOR = 'MODEL_ARMOR';
}
4 changes: 2 additions & 2 deletions src/Exceptions/ErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ public function getErrorMessage(): string
/**
* Returns the error status.
*/
public function getErrorStatus(): ?string
public function getErrorStatus(): string
{
return $this->contents['status'];
}

/**
* Returns the error code.
*/
public function getErrorCode(): string|int|null
public function getErrorCode(): int
{
return $this->contents['code'];
}
Expand Down
12 changes: 6 additions & 6 deletions src/Requests/GenerativeModel/GenerateContentRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ protected function defaultBody(): array
{
return [
'contents' => array_map(
static fn (Content $content): array => $content->toArray(),
$this->partsToContents(...$this->parts)
callback: static fn (Content $content): array => $content->toArray(),
array: $this->partsToContents(...$this->parts)
),
'tools' => array_map(
static fn (Tool $tool): array => $tool->toArray(),
$this->tools ?? []
callback: static fn (Tool $tool): array => $tool->toArray(),
array: $this->tools
),
'toolConfig' => $this->toolConfig?->toArray(),
'safetySettings' => array_map(
static fn (SafetySetting $setting): array => $setting->toArray(),
$this->safetySettings ?? []
callback: static fn (SafetySetting $setting): array => $setting->toArray(),
array: $this->safetySettings
),
'systemInstruction' => $this->systemInstruction?->toArray(),
'generationConfig' => $this->generationConfig?->toArray(),
Expand Down
12 changes: 6 additions & 6 deletions src/Requests/GenerativeModel/StreamGenerateContentRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ protected function defaultBody(): array
{
return [
'contents' => array_map(
static fn (Content $content): array => $content->toArray(),
$this->partsToContents(...$this->parts)
callback: static fn (Content $content): array => $content->toArray(),
array: $this->partsToContents(...$this->parts)
),
'tools' => array_map(
static fn (Tool $tool): array => $tool->toArray(),
$this->tools ?? []
callback: static fn (Tool $tool): array => $tool->toArray(),
array: $this->tools
),
'toolConfig' => $this->toolConfig?->toArray(),
'safetySettings' => array_map(
static fn (SafetySetting $setting): array => $setting->toArray(),
$this->safetySettings ?? []
callback: static fn (SafetySetting $setting): array => $setting->toArray(),
array: $this->safetySettings
),
'systemInstruction' => $this->systemInstruction?->toArray(),
'generationConfig' => $this->generationConfig?->toArray(),
Expand Down
44 changes: 44 additions & 0 deletions src/Resources/ChatSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
use Gemini\Contracts\Resources\ChatSessionContract;
use Gemini\Data\Blob;
use Gemini\Data\Content;
use Gemini\Data\Part;
use Gemini\Data\UploadedFile;
use Gemini\Enums\Role;
use Gemini\Responses\GenerativeModel\GenerateContentResponse;
use Generator;

/**
* https://ai.google.dev/tutorials/rest_quickstart#multi-turn_conversations_chat
Expand Down Expand Up @@ -43,6 +46,47 @@ public function sendMessage(string|Blob|array|Content|UploadedFile ...$parts): G
return $response;
}

/**
* @param string|Blob|array<string|Blob|UploadedFile>|Content|UploadedFile ...$parts
* @return Generator<int, GenerateContentResponse>
*/
public function streamSendMessage(string|Blob|array|Content|UploadedFile ...$parts): Generator
{
$this->history = array_merge($this->history, $this->partsToContents(...$parts));

$stream = $this->model->streamGenerateContent(...$this->history);

/** @var array<Part> $parts */
$parts = [];

foreach ($stream as $response) {
/** @var GenerateContentResponse $response */
if (empty($response->candidates) === false) {
foreach ($response->parts() as $index => $part) {
if (isset($parts[$index]) === false) {
$parts[$index] = $part;

continue;
}

$parts[$index] = Part::from(
attributes: array_merge( /* @phpstan-ignore argument.type */
$parts[$index]->toArray(),
['text' => ($parts[$index]->text ?? '').($part->text ?? '')]
)
);
}
}

yield $response;
}

$this->history[] = new Content(
parts: $parts,
role: Role::MODEL,
);
}

/**
* @param array<Content> $history
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Resources/GenerativeModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ public function generateContent(string|Blob|array|Content|UploadedFile ...$parts
* Generates a streamed response from the model given an input GenerateContentRequest.
*
* @see https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
*
* @return StreamResponse<GenerateContentResponse>
*/
public function streamGenerateContent(string|Blob|array|Content|UploadedFile ...$parts): StreamResponse
{
Expand Down
6 changes: 3 additions & 3 deletions src/Responses/Files/MetadataResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function __construct(
public readonly string $name,
public readonly string $displayName,
public readonly string $mimeType,
public readonly string $sizeBytes,
public readonly ?string $sizeBytes,
public readonly string $createTime,
public readonly string $updateTime,
public readonly string $expirationTime,
Expand All @@ -31,15 +31,15 @@ public function __construct(
) {}

/**
* @param array{ name: string, displayName: string, mimeType: string, sizeBytes: string, createTime: string, updateTime: string, expirationTime: string, sha256Hash: string, uri: string, state: string, videoMetadata: ?array{ videoDuration: string } } $attributes
* @param array{ name: string, displayName: string, mimeType: string, sizeBytes: ?string, createTime: string, updateTime: string, expirationTime: string, sha256Hash: string, uri: string, state: string, videoMetadata: ?array{ videoDuration: string } } $attributes
*/
public static function from(array $attributes): self
{
return new self(
name: $attributes['name'],
displayName: $attributes['displayName'],
mimeType: $attributes['mimeType'],
sizeBytes: $attributes['sizeBytes'],
sizeBytes: $attributes['sizeBytes'] ?? null,
createTime: $attributes['createTime'],
updateTime: $attributes['updateTime'],
expirationTime: $attributes['expirationTime'],
Expand Down
5 changes: 5 additions & 0 deletions src/Responses/StreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ final class StreamResponse implements IteratorAggregate

/**
* Creates a new Stream ResponseDTO instance.
*
* @param class-string<T> $responseClass
*/
public function __construct(
private readonly string $responseClass,
Expand All @@ -29,6 +31,9 @@ public function __construct(
//
}

/**
* @return Generator<int, T>
*/
public function getIterator(): Generator
{
while (! $this->response->getBody()->eof()) {
Expand Down
9 changes: 8 additions & 1 deletion src/Testing/Resources/ChatSessionTestResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use Gemini\Contracts\Resources\ChatSessionContract;
use Gemini\Data\Blob;
use Gemini\Data\Content;
use Gemini\Data\UploadedFile;
use Gemini\Resources\ChatSession;
use Gemini\Responses\GenerativeModel\GenerateContentResponse;
use Gemini\Testing\Resources\Concerns\Testable;
use Generator;

final class ChatSessionTestResource implements ChatSessionContract
{
Expand All @@ -20,11 +22,16 @@ protected function resource(): string
return ChatSession::class;
}

public function sendMessage(string|array|Blob|Content ...$parts): GenerateContentResponse
public function sendMessage(string|Blob|array|Content|UploadedFile ...$parts): GenerateContentResponse
{
return $this->record(method: __FUNCTION__, args: func_get_args(), model: $this->model);
}

public function streamSendMessage(string|Blob|array|Content|UploadedFile ...$parts): Generator
{
yield $this->record(method: __FUNCTION__, args: func_get_args(), model: $this->model);
}

/**
* @param array<Content> $history
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Transporters/DTOs/ResponseDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Gemini\Transporters\DTOs;

/**
* @template-covariant TData of array
* @template TData of array
*
* @internal
*/
Expand Down
Loading