├── src ├── Exceptions │ ├── BaseException.php │ ├── ApiException.php │ ├── NetworkException.php │ ├── StreamException.php │ ├── ValidationException.php │ ├── AuthenticationException.php │ └── RateLimitException.php ├── Helpers │ └── helpers.php ├── Enums │ └── Capability.php ├── Responses │ ├── TextResponse.php │ ├── VideoResponse.php │ ├── ImageResponse.php │ ├── BaseResponse.php │ ├── FileResponse.php │ ├── CacheResponse.php │ └── AudioResponse.php ├── Builders │ ├── TextBuilder.php │ ├── ImageBuilder.php │ ├── VideoBuilder.php │ ├── FileBuilder.php │ ├── AudioBuilder.php │ ├── CacheBuilder.php │ └── BaseBuilder.php ├── Factory │ └── ProviderFactory.php ├── Console │ └── ModelsCommand.php ├── Contracts │ └── ProviderInterface.php ├── GeminiServiceProvider.php ├── Facades │ └── Gemini.php ├── Gemini.php ├── Http │ └── HttpClient.php ├── Providers │ ├── BaseProvider.php │ └── GeminiProvider.php └── Config │ └── gemini.php ├── LICENSE.md ├── composer.json ├── CONTRIBUTING.md └── README.md /src/Exceptions/BaseException.php: -------------------------------------------------------------------------------- 1 | data['candidates'][0]['content']['parts'][0]['text'] ?? ''; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Exceptions/StreamException.php: -------------------------------------------------------------------------------- 1 | retryAfter = $retryAfter; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Responses/VideoResponse.php: -------------------------------------------------------------------------------- 1 | data['video'] ?? ''; 10 | } 11 | 12 | public function url(): string 13 | { 14 | return $this->data['uri'] ?? ''; 15 | } 16 | 17 | public function save(string $path): void 18 | { 19 | file_put_contents($path, $this->content()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Builders/TextBuilder.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | 15 | public function generate(): TextResponse 16 | { 17 | return $this->provider->generateText($this->params); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Builders/ImageBuilder.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | 15 | public function generate(): ImageResponse 16 | { 17 | return $this->provider->generateImage($this->params); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Builders/VideoBuilder.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | 15 | public function generate(): VideoResponse 16 | { 17 | return $this->provider->generateVideo($this->params); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Factory/ProviderFactory.php: -------------------------------------------------------------------------------- 1 | table(['Model', 'Name', 'Version', 'Capabilities'], $tableData); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Responses/ImageResponse.php: -------------------------------------------------------------------------------- 1 | data['candidates'][0]['content']['parts'] as $parts){ 12 | if(key_exists('inlineData', $parts)) 13 | $part = $parts; 14 | } 15 | if(!isset($part)) 16 | throw new ApiException("Failed to retrieve image content. No inlineData found."); 17 | return base64_decode($part['inlineData']['data']); 18 | } 19 | 20 | public function url(): string 21 | { 22 | return $this->data['uri'] ?? ''; 23 | } 24 | 25 | public function save(string $path): void 26 | { 27 | file_put_contents($path, $this->content()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Responses/BaseResponse.php: -------------------------------------------------------------------------------- 1 | data = $data; 12 | } 13 | 14 | abstract public function content(): string; 15 | 16 | public function toArray(): array 17 | { 18 | return $this->data; 19 | } 20 | 21 | public function json(): string 22 | { 23 | return json_encode($this->data); 24 | } 25 | 26 | public function model(): string 27 | { 28 | return $this->data['modelVersion'] ?? ''; 29 | } 30 | 31 | public function usage(): array 32 | { 33 | return $this->data['usageMetadata'] ?? []; 34 | } 35 | 36 | public function requestId(): string 37 | { 38 | return $this->data['responseId'] ?? ''; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Contracts/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | data = $data; 14 | } 15 | 16 | public function toArray(): array 17 | { 18 | return $this->data; 19 | } 20 | 21 | public function files(): array 22 | { 23 | return $this->data['files'] ?? []; 24 | } 25 | 26 | public function name(): ?string 27 | { 28 | return $this->data['name'] ?? null; 29 | } 30 | 31 | public function uri(): ?string 32 | { 33 | return $this->data['uri'] ?? null; 34 | } 35 | 36 | public function state(): ?string 37 | { 38 | return $this->data['state'] ?? null; 39 | } 40 | 41 | public function mimeType(): ?string 42 | { 43 | return $this->data['mimeType'] ?? null; 44 | } 45 | 46 | public function displayName(): ?string 47 | { 48 | return $this->data['displayName'] ?? null; 49 | } 50 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hossein Hezami 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 | -------------------------------------------------------------------------------- /src/Responses/CacheResponse.php: -------------------------------------------------------------------------------- 1 | data = $data; 12 | } 13 | 14 | public function toArray(): array 15 | { 16 | return $this->data; 17 | } 18 | 19 | public function name(): string 20 | { 21 | return $this->data['name'] ?? ''; 22 | } 23 | 24 | public function model(): string 25 | { 26 | return $this->data['model'] ?? ''; 27 | } 28 | 29 | public function createTime(): string 30 | { 31 | return $this->data['createTime'] ?? ''; 32 | } 33 | 34 | public function updateTime(): string 35 | { 36 | return $this->data['updateTime'] ?? ''; 37 | } 38 | 39 | public function expireTime(): string 40 | { 41 | return $this->data['expireTime'] ?? ''; 42 | } 43 | 44 | public function displayName(): string 45 | { 46 | return $this->data['displayName'] ?? ''; 47 | } 48 | 49 | public function usageMetadata(): array 50 | { 51 | return $this->data['usageMetadata'] ?? []; 52 | } 53 | } -------------------------------------------------------------------------------- /src/GeminiServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/Config/gemini.php', 'gemini'); 18 | 19 | $this->app->singleton(ProviderFactory::class, function ($app) { 20 | return new ProviderFactory(); 21 | }); 22 | 23 | $this->app->singleton('gemini', function ($app) { 24 | return new Gemini($app->make(ProviderFactory::class)); 25 | }); 26 | } 27 | 28 | /** 29 | * Bootstrap services 30 | */ 31 | public function boot(): void 32 | { 33 | $this->publishes([ 34 | __DIR__ . '/Config/gemini.php' => config_path('gemini.php'), 35 | ], ['gemini-config']); 36 | 37 | if ($this->app->runningInConsole()) { 38 | $this->commands([ 39 | ModelsCommand::class, 40 | ]); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hosseinhezami/laravel-gemini", 3 | "description": "A production-ready Laravel package to integrate with the Google Gemini API. Supports text, image, video, audio, long-context, structured output, files, caching, function-calling and understanding capabilities.", 4 | "keywords": ["laravel", "gemini", "ai", "api", "google"], 5 | "version": "1.0.4", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Hossein Hezami", 11 | "email": "hossein.hezami@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.2|^8.3|^8.4|^8.5", 16 | "illuminate/support": "^10.0|^11.0|^12.0", 17 | "illuminate/http": "^10.0|^11.0|^12.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "HosseinHezami\\LaravelGemini\\": "src/" 22 | }, 23 | "files": [ 24 | "src/Helpers/helpers.php" 25 | ] 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "HosseinHezami\\LaravelGemini\\GeminiServiceProvider" 31 | ], 32 | "aliases": { 33 | "Gemini": "HosseinHezami\\LaravelGemini\\Facades\\Gemini" 34 | } 35 | } 36 | }, 37 | "scripts": { 38 | "post-autoload-dump": [ 39 | "@php artisan vendor:publish --tag=gemini-config --force" 40 | ] 41 | }, 42 | "minimum-stability": "stable", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /src/Facades/Gemini.php: -------------------------------------------------------------------------------- 1 | text(); 23 | } 24 | 25 | public static function image(): ImageBuilder 26 | { 27 | return static::getFacadeRoot()->image(); 28 | } 29 | 30 | public static function video(): VideoBuilder 31 | { 32 | return static::getFacadeRoot()->video(); 33 | } 34 | 35 | public static function audio(): AudioBuilder 36 | { 37 | return static::getFacadeRoot()->audio(); 38 | } 39 | 40 | public static function files(): FileBuilder 41 | { 42 | return static::getFacadeRoot()->files(); 43 | } 44 | 45 | public static function caches(): CacheBuilder 46 | { 47 | return static::getFacadeRoot()->caches(); 48 | } 49 | 50 | public static function setApiKey(string $apiKey): self 51 | { 52 | static::getFacadeRoot()->setApiKey($apiKey); 53 | return new static; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Builders/FileBuilder.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 17 | } 18 | 19 | public function upload(string $fileType, string $filePath): string 20 | { 21 | if (empty($fileType) || empty($filePath)) { 22 | throw new ValidationException('File type and path are required for upload.'); 23 | } 24 | $this->params['fileType'] = $fileType; 25 | $this->params['filePath'] = $filePath; 26 | return $this->provider->uploadFile($this->params); 27 | } 28 | 29 | public function get(string $fileName): FileResponse 30 | { 31 | if (empty($fileName)) { 32 | throw new ValidationException('File name is required.'); 33 | } 34 | return $this->provider->getFile($fileName); 35 | } 36 | 37 | public function delete(string $fileName): bool 38 | { 39 | if (empty($fileName)) { 40 | throw new ValidationException('File name is required.'); 41 | } 42 | return $this->provider->deleteFile($fileName); 43 | } 44 | 45 | public function list(array $params = []): FileResponse 46 | { 47 | $this->params = array_merge($this->params, $params); 48 | return $this->provider->listFiles($this->params); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Gemini.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 22 | } 23 | 24 | public function setApiKey(string $apiKey): self 25 | { 26 | $this->apiKey = $apiKey; 27 | return $this; 28 | } 29 | 30 | public function text(): TextBuilder 31 | { 32 | return new TextBuilder($this->getProvider()); 33 | } 34 | 35 | public function image(): ImageBuilder 36 | { 37 | return new ImageBuilder($this->getProvider()); 38 | } 39 | 40 | public function video(): VideoBuilder 41 | { 42 | return new VideoBuilder($this->getProvider()); 43 | } 44 | 45 | public function audio(): AudioBuilder 46 | { 47 | return new AudioBuilder($this->getProvider()); 48 | } 49 | 50 | public function files(): FileBuilder 51 | { 52 | return new FileBuilder($this->getProvider()); 53 | } 54 | 55 | public function caches(): CacheBuilder 56 | { 57 | return new CacheBuilder($this->getProvider()); 58 | } 59 | 60 | public function models() 61 | { 62 | return $this->getProvider()->models(); 63 | } 64 | 65 | public function embeddings(array $params): array 66 | { 67 | return $this->getProvider()->embeddings($params); 68 | } 69 | 70 | public function getProvider(?string $alias = null) 71 | { 72 | return $this->factory->create($alias, $this->apiKey); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Builders/AudioBuilder.php: -------------------------------------------------------------------------------- 1 | value; 14 | } 15 | 16 | /** 17 | * Set voice name for single-speaker TTS. 18 | * 19 | * @param string $voiceName e.g., 'Kore', 'Puck' 20 | * @return self 21 | */ 22 | public function voiceName(string $voiceName): self 23 | { 24 | $this->params['voiceName'] = $voiceName; 25 | $this->params['multiSpeaker'] = false; 26 | return $this; 27 | } 28 | 29 | /** 30 | * Set speaker voices for multi-speaker TTS. 31 | * 32 | * @param array $speakerVoices e.g., [['speaker' => 'Joe', 'voiceName' => 'Kore'], ['speaker' => 'Jane', 'voiceName' => 'Puck']] 33 | * @return self 34 | */ 35 | public function speakerVoices(array $speakerVoices): self 36 | { 37 | $this->params['speakerVoices'] = $speakerVoices; 38 | $this->params['multiSpeaker'] = true; 39 | return $this; 40 | } 41 | 42 | public function generate(): AudioResponse 43 | { 44 | // Validate voiceName for single-speaker 45 | if (!isset($this->params['multiSpeaker']) || !$this->params['multiSpeaker']) { 46 | if (!isset($this->params['voiceName']) || empty($this->params['voiceName'])) { 47 | throw new ValidationException('Voice name is required for single-speaker TTS.'); 48 | } 49 | } 50 | // Validate speakerVoices for multi-speaker 51 | if (isset($this->params['multiSpeaker']) && $this->params['multiSpeaker']) { 52 | if (!isset($this->params['speakerVoices']) || empty($this->params['speakerVoices'])) { 53 | throw new ValidationException('Speaker voices are required for multi-speaker TTS.'); 54 | } 55 | } 56 | return $this->provider->generateAudio($this->params); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Http/HttpClient.php: -------------------------------------------------------------------------------- 1 | client = Http::baseUrl($baseUrl) 19 | ->withHeaders(['x-goog-api-key' => $apiKey]) 20 | ->timeout(config('gemini.timeout')) 21 | ->retry(config('gemini.retry_policy.max_retries'), config('gemini.retry_policy.retry_delay'), function ($exception, $request) { 22 | if ($exception instanceof RateLimitException) { 23 | sleep($exception->retryAfter ?? 1); 24 | return true; 25 | } 26 | return $exception->response->status() >= 500; 27 | }); 28 | } 29 | 30 | public function withHeaders(array $headers): self 31 | { 32 | $this->client = $this->client->withHeaders($headers); 33 | return $this; 34 | } 35 | 36 | public function withBody($content, string $contentType = 'application/json'): self 37 | { 38 | $this->client = $this->client->withBody($content, $contentType); 39 | return $this; 40 | } 41 | 42 | public function withOptions(array $options): self 43 | { 44 | $this->client = $this->client->withOptions($options); 45 | return $this; 46 | } 47 | 48 | public function post(string $url, array $data = []): \Illuminate\Http\Client\Response 49 | { 50 | return $this->client->post($url, $data); 51 | } 52 | 53 | public function get(string $url): \Illuminate\Http\Client\Response 54 | { 55 | return $this->client->get($url); 56 | } 57 | 58 | public function patch(string $url, array $data): \Illuminate\Http\Client\Response 59 | { 60 | return $this->client->patch($url, $data); 61 | } 62 | 63 | public function delete(string $url): \Illuminate\Http\Client\Response 64 | { 65 | return $this->client->delete($url); 66 | } 67 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Laravel Gemini 2 | 3 | Thank you for considering contributing to **Laravel Gemini**! 4 | We welcome all kinds of contributions, including bug reports, feature requests, documentation improvements, and code contributions. 5 | 6 | --- 7 | 8 | ## How to Contribute 9 | 10 | ### 1. Fork the Repository 11 | - Click the **Fork** button on the top right of the repository page. 12 | - Clone your fork locally: 13 | ```bash 14 | git clone https://github.com/hosseinhezami/laravel-gemini.git 15 | cd laravel-gemini 16 | ```` 17 | 18 | ### 2. Create a Feature Branch 19 | 20 | * Always work on a new branch: 21 | 22 | ```bash 23 | git checkout -b feature/your-feature-name 24 | ``` 25 | * Use descriptive branch names such as: 26 | 27 | * `feature/add-cache` 28 | * `fix/files-upload-bug` 29 | * `docs/update-readme` 30 | 31 | ### 3. Install Dependencies 32 | 33 | * Install required dependencies using Composer: 34 | 35 | ```bash 36 | composer install 37 | ``` 38 | 39 | ### 4. Run Tests 40 | 41 | * Make sure all tests pass before submitting your changes: 42 | 43 | ```bash 44 | php artisan test 45 | ``` 46 | * If you add new functionality, write corresponding tests. 47 | 48 | ### 5. Commit Guidelines 49 | 50 | * Follow conventional commits where possible: 51 | 52 | * `feat: add new builder feature` 53 | * `fix: resolve caching issue in TextBuilder sync` 54 | * `docs: update installation guide` 55 | 56 | ### 6. Push Changes 57 | 58 | * Push your branch to your fork: 59 | 60 | ```bash 61 | git push origin feature/your-feature-name 62 | ``` 63 | 64 | ### 7. Submit a Pull Request 65 | 66 | * Open a PR against the `master` branch. 67 | * Clearly describe: 68 | 69 | * The purpose of your changes. 70 | * Any issues it fixes (e.g., `Fixes #12`). 71 | * Additional notes for reviewers. 72 | 73 | --- 74 | 75 | ## Code Style 76 | 77 | * Follow **PSR-12** coding standards. 78 | * Use **PHPStan** or **Laravel Pint** for static analysis and code style fixes. 79 | * Keep functions small and focused. 80 | * Write meaningful docblocks for public methods. 81 | 82 | --- 83 | 84 | ## Reporting Issues 85 | 86 | If you find a bug, please open an issue with: 87 | 88 | * A clear title. 89 | * Steps to reproduce the bug. 90 | * Expected vs actual behavior. 91 | * Laravel version and PHP version. 92 | 93 | --- 94 | 95 | ## Community Guidelines 96 | 97 | * Be respectful and collaborative. 98 | * Avoid duplicate issues/PRs—search before creating new ones. 99 | * Keep discussions constructive and focused on improving the project. 100 | 101 | --- 102 | 103 | ## License 104 | 105 | By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE.md). 106 | -------------------------------------------------------------------------------- /src/Builders/CacheBuilder.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 16 | } 17 | 18 | // Create a cached content with direct parameters 19 | public function create( 20 | string $model, 21 | array $contents, 22 | ?string $systemInstruction = null, 23 | array $tools = [], 24 | array $toolConfig = [], 25 | ?string $displayName = null, 26 | ?string $ttl = null, 27 | ?string $expireTime = null 28 | ): CacheResponse { 29 | if (empty($model) || empty($contents)) { 30 | throw new ValidationException('Model and contents are required for creating cache.'); 31 | } 32 | 33 | $params = compact('model', 'contents', 'systemInstruction', 'tools', 'toolConfig', 'displayName', 'ttl', 'expireTime'); 34 | return $this->provider->createCachedContent($params); 35 | } 36 | 37 | // List cached contents with optional params 38 | public function list(?int $pageSize = null, ?string $pageToken = null): CacheResponse 39 | { 40 | $params = array_filter(compact('pageSize', 'pageToken')); 41 | return $this->provider->listCachedContents($params); 42 | } 43 | 44 | // Get a cached content by name 45 | public function get(string $name): CacheResponse 46 | { 47 | if (empty($name)) { 48 | throw new ValidationException('Cache name is required.'); 49 | } 50 | return $this->provider->getCachedContent($name); 51 | } 52 | 53 | // Update cache expiration 54 | public function update( 55 | string $name, 56 | ?string $ttl = null, 57 | ?string $expireTime = null 58 | ): CacheResponse { 59 | if (empty($name)) { 60 | throw new ValidationException('Cache name is required.'); 61 | } 62 | if (empty($ttl) && empty($expireTime)) { 63 | throw new ValidationException('TTL or expireTime is required for update.'); 64 | } 65 | 66 | $expiration = array_filter(compact('ttl', 'expireTime')); 67 | return $this->provider->updateCachedContent($name, $expiration); 68 | } 69 | 70 | // Delete a cached content by name 71 | public function delete(string $name): bool 72 | { 73 | if (empty($name)) { 74 | throw new ValidationException('Cache name is required.'); 75 | } 76 | return $this->provider->deleteCachedContent($name); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Responses/AudioResponse.php: -------------------------------------------------------------------------------- 1 | data['candidates'][0]['finishReason']) && $this->data['candidates'][0]['finishReason'] != 'STOP') { 11 | $finishReason = $this->data['candidates'][0]['finishReason'] ?? 'UNKNOWN'; 12 | throw new ApiException("Failed to retrieve audio content. Finish reason: {$finishReason}"); 13 | } 14 | return base64_decode($this->data['candidates'][0]['content']['parts'][0]['inlineData']['data']); 15 | } 16 | 17 | /** 18 | * Extract audio metadata from mimeType 19 | */ 20 | public function getAudioMeta(): array 21 | { 22 | $inlineData = $this->data['candidates'][0]['content']['parts'][0]['inlineData'] ?? []; 23 | $mime = $inlineData['mimeType'] ?? null; 24 | 25 | // Defaults 26 | $sampleRate = 16000; 27 | $channels = 1; 28 | $bitsPerSample = 16; 29 | 30 | if ($mime && str_starts_with($mime, 'audio/L16')) { 31 | $parts = explode(';', $mime); 32 | foreach ($parts as $part) { 33 | $part = trim($part); 34 | if (str_starts_with($part, 'rate=')) { 35 | $sampleRate = (int)substr($part, 5); 36 | } elseif (str_starts_with($part, 'channels=')) { 37 | $channels = (int)substr($part, 9); 38 | } 39 | } 40 | } 41 | 42 | return [ 43 | 'mimeType' => $mime, 44 | 'sampleRate' => $sampleRate, 45 | 'channels' => $channels, 46 | 'bitsPerSample' => $bitsPerSample, 47 | ]; 48 | } 49 | 50 | public function save(string $path): void 51 | { 52 | $raw = $this->content(); 53 | $meta = $this->getAudioMeta(); 54 | 55 | if ($meta['mimeType'] && str_starts_with($meta['mimeType'], 'audio/L16')) { 56 | // Wrap raw PCM to WAV 57 | $wav = $this->pcmToWav( 58 | $raw, 59 | $meta['sampleRate'], 60 | $meta['channels'], 61 | $meta['bitsPerSample'] 62 | ); 63 | file_put_contents($path, $wav); 64 | } else { 65 | // fallback: raw dump 66 | file_put_contents($path, $raw); 67 | } 68 | } 69 | 70 | private function pcmToWav(string $pcmData, int $sampleRate, int $channels, int $bitsPerSample): string 71 | { 72 | $byteRate = $sampleRate * $channels * $bitsPerSample / 8; 73 | $blockAlign = $channels * $bitsPerSample / 8; 74 | $dataSize = strlen($pcmData); 75 | 76 | // WAV header 77 | $header = pack('N4', 0x52494646, 36 + $dataSize, 0x57415645, 0x666d7420); 78 | $header .= pack('V', 16); // Subchunk1Size 79 | $header .= pack('v', 1); // PCM format 80 | $header .= pack('v', $channels); 81 | $header .= pack('V', $sampleRate); 82 | $header .= pack('V', $byteRate); 83 | $header .= pack('v', $blockAlign); 84 | $header .= pack('v', $bitsPerSample); 85 | $header .= pack('N2', 0x64617461, $dataSize); 86 | 87 | return $header . $pcmData; 88 | } 89 | } -------------------------------------------------------------------------------- /src/Providers/BaseProvider.php: -------------------------------------------------------------------------------- 1 | http = new HttpClient(apiKey: $apiKey); 16 | } 17 | 18 | protected function handleResponse($response, string $type) 19 | { 20 | if ($response->failed()) { 21 | $status = $response->status(); 22 | if ($status === 401) { 23 | throw new Exceptions\AuthenticationException(); 24 | } elseif ($status === 429) { 25 | throw new Exceptions\RateLimitException(retryAfter: $response->header('Retry-After')); 26 | } elseif ($status >= 500) { 27 | throw new Exceptions\ApiException(); 28 | } elseif ($status === 400) { 29 | throw new Exceptions\ValidationException(); 30 | } else { 31 | throw new Exceptions\NetworkException(); 32 | } 33 | } 34 | 35 | $data = $response->json(); 36 | 37 | return new ("HosseinHezami\\LaravelGemini\\Responses\\" . $type . "Response")($data); 38 | } 39 | 40 | /** 41 | * Upload a file to Gemini API and return its URI. 42 | * 43 | * @param string $fileType Type of file (e.g., 'image', 'video', 'audio', 'document') 44 | * @param string $filePath Path to the file 45 | * @return string File URI returned by the API 46 | * @throws Exceptions\ValidationException If file type is invalid or file not found 47 | * @throws Exceptions\ApiException 48 | * @throws Exceptions\RateLimitException 49 | */ 50 | protected function upload(string $fileType, string $filePath): string 51 | { 52 | if (!file_exists($filePath) || !is_readable($filePath)) { 53 | throw new Exceptions\ValidationException("File does not exist or is not readable: {$filePath}"); 54 | } 55 | 56 | $validTypes = ['image', 'video', 'audio', 'document']; 57 | if (!in_array($fileType, $validTypes)) { 58 | throw new Exceptions\ValidationException("Invalid file type: {$fileType}. Allowed types: " . implode(', ', $validTypes)); 59 | } 60 | 61 | $mimeType = $this->getMimeType($fileType, $filePath); 62 | $fileSize = filesize($filePath); 63 | $displayName = basename($filePath); 64 | 65 | /** 66 | * Step 1: Initiate resumable upload session 67 | */ 68 | $initialResponse = $this->http 69 | ->withHeaders([ 70 | 'Content-Type' => 'application/json', 71 | ]) 72 | ->post('/upload/v1beta/files?uploadType=resumable', [ 73 | 'file' => [ 74 | 'display_name' => $displayName, 75 | ], 76 | ]); 77 | 78 | $uploadUrl = $initialResponse->header('Location'); 79 | 80 | if (!$uploadUrl) { 81 | throw new Exceptions\ApiException('Upload URL not received from API'); 82 | } 83 | 84 | /** 85 | * Step 2: Upload the entire file in a single chunk and finalize 86 | */ 87 | $uploadResponse = $this->http 88 | ->withHeaders([ 89 | 'Content-Range' => "bytes 0-" . ($fileSize - 1) . "/$fileSize", 90 | ]) 91 | ->withBody( 92 | file_get_contents($filePath), 93 | $mimeType 94 | ) 95 | ->post($uploadUrl); 96 | 97 | if (!$uploadResponse->successful()) { 98 | throw new Exceptions\ApiException('Upload failed: ' . $uploadResponse->body()); 99 | } 100 | 101 | $json = $uploadResponse->json(); 102 | 103 | if (!isset($json['file']['uri'])) { 104 | throw new Exceptions\ApiException('File URI not found in API response'); 105 | } 106 | 107 | return $json['file']['uri']; 108 | } 109 | 110 | protected function getMimeType(string $fileType, string $filePath): string 111 | { 112 | $mimeTypes = [ 113 | 'image' => ['png' => 'image/png', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'webp' => 'image/webp', 'heic' => 'image/heic', 'heif' => 'image/heif'], 114 | 'video' => ['mp4' => 'video/mp4', 'mpeg' => 'video/mpeg', 'mov' => 'video/mov', 'avi' => 'video/avi', 'flv' => 'video/x-flv', 'mpg' => 'video/mpg', 'webm' => 'video/webm', 'wmv' => 'video/wmv', '3gpp' => 'video/3gpp'], 115 | 'audio' => ['wav' => 'audio/x-wav', 'mp3' => 'audio/mp3', 'aiff' => 'audio/aiff', 'aac' => 'audio/aac', 'ogg' => 'audio/ogg', 'flac' => 'audio/flac'], 116 | 'document' => ['pdf' => 'application/pdf', 'txt' => 'text/plain', 'md' => 'text/markdown'] 117 | ]; 118 | 119 | $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); 120 | return $mimeTypes[$fileType][$extension] ?? throw new Exceptions\ValidationException("Unsupported {$fileType} format: {$extension}"); 121 | } 122 | } -------------------------------------------------------------------------------- /src/Config/gemini.php: -------------------------------------------------------------------------------- 1 | env('GEMINI_API_KEY', ''), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Base URI 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The base URI for the Gemini API. 23 | | 24 | */ 25 | 26 | 'base_uri' => env('GEMINI_BASE_URI', 'https://generativelanguage.googleapis.com'), 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Default Provider 31 | |-------------------------------------------------------------------------- 32 | | 33 | | The default provider to use for API requests. 34 | | 35 | */ 36 | 37 | 'default_provider' => env('GEMINI_DEFAULT_PROVIDER', 'gemini'), 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Providers 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Configuration for different providers. Each provider should specify 45 | | the class and default models and methods for different capabilities. 46 | | 47 | */ 48 | 49 | 'providers' => [ 50 | 'gemini' => [ 51 | 'class' => \HosseinHezami\LaravelGemini\Providers\GeminiProvider::class, 52 | 'models' => [ 53 | 'text' => 'gemini-2.5-flash-lite', 54 | 'image' => 'gemini-2.5-flash-image-preview', 55 | 'video' => 'veo-3.0-fast-generate-001', 56 | 'audio' => 'gemini-2.5-flash-preview-tts', 57 | 'embedding' => 'gemini-embedding-001', 58 | ], 59 | 'methods' => [ 60 | 'text' => 'generateContent', 61 | 'image' => 'generateContent', // generateContent, predict 62 | 'video' => 'predictLongRunning', 63 | 'audio' => 'generateContent', 64 | ], 65 | /** 66 | * Set voice name for single-speaker TTS. 67 | * @param string $voiceName e.g., 'Kore', 'Puck' 68 | * Set speaker voices for multi-speaker TTS. 69 | * @param array $speakerVoices e.g., [['speaker' => 'Joe', 'voiceName' => 'Kore'], ['speaker' => 'Jane', 'voiceName' => 'Puck']] 70 | */ 71 | 'default_speech_config' => [ 72 | 'voiceName' => 'Kore' 73 | ], 74 | ], 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Request Timeout 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The timeout in seconds for API requests. 83 | | 84 | */ 85 | 86 | 'timeout' => env('GEMINI_TIMEOUT', 30), 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Retry Policy 91 | |-------------------------------------------------------------------------- 92 | | 93 | | Configuration for retrying failed requests. 94 | | 95 | */ 96 | 97 | 'retry_policy' => [ 98 | 'max_retries' => env('GEMINI_MAX_RETRIES', 30), 99 | 'retry_delay' => env('GEMINI_RETRY_DELAY', 1000), // milliseconds 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Safety Settings 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Default safety settings for content generation. 108 | | 109 | */ 110 | 111 | 'safety_settings' => [ 112 | [ 113 | 'category' => 'HARM_CATEGORY_HARASSMENT', 114 | 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' 115 | ], 116 | [ 117 | 'category' => 'HARM_CATEGORY_HATE_SPEECH', 118 | 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' 119 | ], 120 | [ 121 | 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 122 | 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' 123 | ], 124 | [ 125 | 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 126 | 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' 127 | ], 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Logging 133 | |-------------------------------------------------------------------------- 134 | | 135 | | Enable or disable logging of API requests and responses. 136 | | 137 | */ 138 | 139 | 'logging' => env('GEMINI_LOGGING', false), 140 | 141 | /* 142 | |-------------------------------------------------------------------------- 143 | | Stream Configuration 144 | |-------------------------------------------------------------------------- 145 | | 146 | | Configuration for streaming responses. 147 | | 148 | */ 149 | 150 | 'stream' => [ 151 | 'chunk_size' => env('GEMINI_STREAM_CHUNK_SIZE', 1024), 152 | 'timeout' => env('GEMINI_STREAM_TIMEOUT', 1000), 153 | ], 154 | 155 | /* 156 | |-------------------------------------------------------------------------- 157 | | Caching Configuration 158 | |-------------------------------------------------------------------------- 159 | | 160 | | Cache Configuration. 161 | | 162 | */ 163 | 164 | 'caching' => [ 165 | 'default_ttl' => '3600s', // Default expiration TTL (e.g., '300s', '1h') 166 | 'max_page_size' => 50, // Default page size for listing caches 167 | ], 168 | 169 | ]; 170 | -------------------------------------------------------------------------------- /src/Builders/BaseBuilder.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 18 | $this->params['capability'] = $this->getCapability(); 19 | $this->params['defaultProvider'] = config('gemini.default_provider'); 20 | $this->params['model'] = config('gemini.providers.' . $this->params['defaultProvider'] . '.models.' . $this->params['capability']); 21 | $this->params['method'] = config('gemini.providers.' . $this->params['defaultProvider'] . '.methods.' . $this->params['capability']); 22 | if (empty($this->params['model'])) { 23 | throw new ValidationException("Default model for {$this->params['capability']} not found in configuration."); 24 | } 25 | if (empty($this->params['method'])) { 26 | throw new ValidationException("Default method for {$this->params['capability']} not found in configuration."); 27 | } 28 | } 29 | 30 | abstract protected function getCapability(): string; 31 | 32 | public function model(string $model = null): self 33 | { 34 | $this->params['model'] = $model ?? $this->params['model']; 35 | return $this; 36 | } 37 | 38 | public function method(string $method): self 39 | { 40 | if (!in_array($method, ['generateContent', 'predict', 'predictLongRunning'])) { 41 | throw new ValidationException("Invalid method: {$method}. Supported: generateContent, predict, predictLongRunning."); 42 | } 43 | $this->params['method'] = $method ?? $this->params['method']; 44 | return $this; 45 | } 46 | 47 | public function prompt(string $prompt): self 48 | { 49 | $this->params['prompt'] = $prompt; 50 | return $this; 51 | } 52 | 53 | public function system(string $system): self 54 | { 55 | $this->params['system'] = $system; 56 | return $this; 57 | } 58 | 59 | public function history(array $history): self 60 | { 61 | $this->params['history'] = $history; 62 | return $this; 63 | } 64 | 65 | public function historyFromModel($query, $bodyColumn, $roleColumn): self 66 | { 67 | $history = $query->get()->map(function ($item) use ($bodyColumn, $roleColumn) { 68 | return ['role' => $item->{$roleColumn}, 'parts' => [['text' => $item->{$bodyColumn}]]]; 69 | })->toArray(); 70 | return $this->history($history); 71 | } 72 | 73 | public function temperature(float $value): self 74 | { 75 | $this->params['temperature'] = $value; 76 | return $this; 77 | } 78 | 79 | public function maxTokens(int $value): self 80 | { 81 | $this->params['maxTokens'] = $value; 82 | return $this; 83 | } 84 | 85 | public function safetySettings(array $settings): self 86 | { 87 | $this->params['safetySettings'] = $settings; 88 | return $this; 89 | } 90 | 91 | public function functionCalls(array $functions): self 92 | { 93 | $this->params['functions'] = $functions; 94 | return $this; 95 | } 96 | 97 | public function structuredSchema(array $schema): self 98 | { 99 | $this->params['structuredSchema'] = $schema; 100 | return $this; 101 | } 102 | 103 | public function upload(string $fileType, string $filePath): self 104 | { 105 | $this->params['fileType'] = $fileType; 106 | $this->params['filePath'] = $filePath; 107 | return $this; 108 | } 109 | 110 | public function file(string $fileType, string $fileUri): self 111 | { 112 | $this->params['fileType'] = $fileType; 113 | $this->params['fileUri'] = $fileUri; 114 | return $this; 115 | } 116 | 117 | public function cache( 118 | ?array $tools = [], 119 | ?array $toolConfig = [], 120 | ?string $displayName = null, 121 | ?string $ttl = null, 122 | ?string $expireTime = null 123 | ): string { 124 | // Build contents from existing params (like prompt, history) 125 | $contents = []; 126 | if (isset($this->params['history'])) { 127 | $contents = array_merge($contents, $this->params['history']); 128 | } 129 | if (isset($this->params['prompt'])) { 130 | $contents[] = ['role' => 'user', 'parts' => [['text' => $this->params['prompt']]]]; 131 | } 132 | 133 | // Use system if set 134 | $systemInstruction = isset($this->params['system']) ? $this->params['system'] : null; 135 | 136 | // Prepare params for provider 137 | $cacheParams = [ 138 | 'model' => $this->params['model'], 139 | 'contents' => $contents, 140 | 'systemInstruction' => $systemInstruction, 141 | 'tools' => $tools, 142 | 'toolConfig' => $toolConfig, 143 | 'displayName' => $displayName, 144 | 'ttl' => $ttl ?? config('gemini.caching.default_ttl'), 145 | 'expireTime' => $expireTime, 146 | ]; 147 | 148 | // Call provider to create 149 | $response = $this->provider->createCachedContent($cacheParams); 150 | 151 | // Return the cache name for chaining or use 152 | return $response->name(); 153 | } 154 | 155 | public function getCache(string $name): CacheResponse 156 | { 157 | if (empty($name)) { 158 | throw new ValidationException('Cache name is required.'); 159 | } 160 | return $this->provider->getCachedContent($name); 161 | } 162 | 163 | public function cachedContent(string $name): self 164 | { 165 | $this->params['cachedContent'] = $name; 166 | return $this; 167 | } 168 | 169 | abstract public function generate(); 170 | 171 | public function stream(callable $callback): void 172 | { 173 | $this->provider->streaming($this->params, $callback); 174 | } 175 | } -------------------------------------------------------------------------------- /src/Providers/GeminiProvider.php: -------------------------------------------------------------------------------- 1 | executeRequest($params, 'Text'); 23 | } 24 | 25 | public function generateImage(array $params): Responses\ImageResponse 26 | { 27 | return $this->executeRequest($params, 'Image'); 28 | } 29 | 30 | public function generateVideo(array $params): Responses\VideoResponse 31 | { 32 | return $this->executeRequest($params, 'Video'); 33 | } 34 | 35 | public function generateAudio(array $params): Responses\AudioResponse 36 | { 37 | return $this->executeRequest($params, 'Audio'); 38 | } 39 | 40 | public function embeddings(array $params): array 41 | { 42 | $response = $this->http->post("/v1beta/models/{$params['model']}:embedContent", $params); 43 | return $response->json(); 44 | } 45 | 46 | public function uploadFile(array $params): string 47 | { 48 | if (!isset($params['fileType']) || !isset($params['filePath'])) { 49 | throw new ValidationException('File type and path are required.'); 50 | } 51 | return $this->upload($params['fileType'], $params['filePath']); 52 | } 53 | 54 | public function listFiles(array $params = []): Responses\FileResponse 55 | { 56 | try { 57 | $response = $this->http->get('/v1beta/files'); 58 | return $this->handleResponse($response, 'File'); 59 | } catch (\Exception $e) { 60 | throw new ApiException("Get files list error: {$e->getMessage()}"); 61 | } 62 | } 63 | 64 | public function getFile(string $fileName): Responses\FileResponse 65 | { 66 | if (empty($fileName)) { 67 | throw new ValidationException('File name is required.'); 68 | } 69 | 70 | try { 71 | $response = $this->http->get("/v1beta/files/{$fileName}"); 72 | return $this->handleResponse($response, 'File'); 73 | } catch (\Exception $e) { 74 | throw new ApiException("Get file error: {$e->getMessage()}"); 75 | } 76 | } 77 | 78 | public function deleteFile(string $fileName): bool 79 | { 80 | if (empty($fileName)) { 81 | throw new ValidationException('File name is required.'); 82 | } 83 | 84 | try { 85 | $response = $this->http->delete("/v1beta/files/{$fileName}"); 86 | return $response->successful(); 87 | } catch (\Exception $e) { 88 | throw new ApiException("Delete file error: {$e->getMessage()}"); 89 | } 90 | } 91 | 92 | // Create cached content 93 | public function createCachedContent(array $params): Responses\CacheResponse 94 | { 95 | $payload = [ 96 | 'model' => "models/{$params['model']}", 97 | 'contents' => $params['contents'], 98 | ]; 99 | 100 | if (!empty($params['systemInstruction'])) { 101 | $payload['systemInstruction'] = ['parts' => [['text' => $params['systemInstruction']]]]; 102 | } 103 | if (!empty($params['tools'])) { 104 | $payload['tools'] = $params['tools']; 105 | } 106 | if (!empty($params['toolConfig'])) { 107 | $payload['toolConfig'] = $params['toolConfig']; 108 | } 109 | if (!empty($params['displayName'])) { 110 | $payload['displayName'] = $params['displayName']; 111 | } 112 | 113 | if (!empty($params['expireTime'])) { 114 | $payload['expireTime'] = $params['expireTime']; 115 | } elseif (!empty($params['ttl'])) { 116 | $payload['ttl'] = $params['ttl'] ?? config('gemini.caching.default_ttl'); 117 | } 118 | 119 | try { 120 | $response = $this->http->post('/v1beta/cachedContents', $payload); 121 | return $this->handleResponse($response, 'Cache'); 122 | } catch (\Exception $e) { 123 | throw new ApiException("Create cache error: {$e->getMessage()}"); 124 | } 125 | } 126 | 127 | // List cached contents 128 | public function listCachedContents(array $params = []): Responses\CacheResponse 129 | { 130 | $queryParams = http_build_query(array_filter([ 131 | 'pageSize' => $params['pageSize'] ?? config('gemini.caching.default_page_size'), 132 | 'pageToken' => $params['pageToken'] ?? null, 133 | ])); 134 | 135 | try { 136 | $response = $this->http->get("/v1beta/cachedContents?{$queryParams}"); 137 | return $this->handleResponse($response, 'Cache'); 138 | } catch (\Exception $e) { 139 | throw new ApiException("List caches error: {$e->getMessage()}"); 140 | } 141 | } 142 | 143 | // Get cached content 144 | public function getCachedContent(string $name): Responses\CacheResponse 145 | { 146 | try { 147 | $response = $this->http->get("/v1beta/$name"); 148 | return $this->handleResponse($response, 'Cache'); 149 | } catch (\Exception $e) { 150 | throw new ApiException("Get cache error: {$e->getMessage()}"); 151 | } 152 | } 153 | 154 | // Update cached content (expiration only) 155 | public function updateCachedContent(string $name, array $expiration): Responses\CacheResponse 156 | { 157 | $payload = []; 158 | 159 | if (!empty($expiration['ttl']) || !empty($expiration['expireTime'])) { 160 | $payload = []; 161 | if (!empty($expiration['expireTime'])) { 162 | $payload['expireTime'] = $expiration['expireTime']; 163 | } elseif (!empty($expiration['ttl'])) { 164 | $payload['ttl'] = $expiration['ttl']; 165 | } else { 166 | $payload['ttl'] = config('gemini.caching.default_ttl'); 167 | } 168 | } else { 169 | throw new ValidationException('TTL or expireTime is required for update.'); 170 | } 171 | 172 | try { 173 | $response = $this->http->patch("/v1beta/{$name}", $payload); 174 | return $this->handleResponse($response, 'Cache'); 175 | } catch (\Exception $e) { 176 | throw new ApiException("Update cache error: {$e->getMessage()}"); 177 | } 178 | } 179 | 180 | // Delete cached content 181 | public function deleteCachedContent(string $name): bool 182 | { 183 | try { 184 | $response = $this->http->delete("/v1beta/$name"); 185 | return $response->successful(); 186 | } catch (\Exception $e) { 187 | throw new ApiException("Delete cache error: {$e->getMessage()}"); 188 | } 189 | } 190 | 191 | public function models(): array 192 | { 193 | $response = $this->http->get('/v1beta/models'); 194 | return $response->json()['models']; 195 | } 196 | 197 | public function streaming(array $params, callable $callback): void 198 | { 199 | $method = $params['method'] ?? 'generateContent'; 200 | if ($method !== 'generateContent') { 201 | throw new ValidationException('Streaming only supported for generateContent method.'); 202 | } 203 | try { 204 | $response = $this->http->withOptions([ 205 | 'stream' => true, 206 | ])->post("/v1beta/models/{$params['model']}:streamGenerateContent", $this->buildRequestBody($params)); 207 | 208 | $body = $response->getBody(); 209 | $buffer = ''; 210 | 211 | while (!$body->eof()) { 212 | $chunk = $body->read(config('gemini.stream.chunk_size', '1024')); 213 | if (!empty($chunk)) { 214 | $buffer .= $chunk; 215 | $lines = explode("\n", $buffer); 216 | $buffer = array_pop($lines); // Keep last incomplete line 217 | 218 | foreach ($lines as $line) { 219 | if (strpos($line, 'data: ') === 0) { 220 | $jsonStr = substr($line, 5); // Remove 'data: ' prefix 221 | $data = json_decode(trim($jsonStr), true); 222 | 223 | if (json_last_error() === JSON_ERROR_NONE) { 224 | $part = $data['candidates'][0]['content']['parts'][0] ?? []; 225 | $callback($part); 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } catch (\Exception $e) { 232 | throw new StreamException( 233 | $e->getMessage(), 234 | $e->getCode(), 235 | $e 236 | ); 237 | } 238 | } 239 | 240 | protected function executeRequest(array $params, string $responseType) 241 | { 242 | $method = $params['method'] ?? 'generateContent'; 243 | $body = $this->buildRequestBody($params, $method === 'predictLongRunning', $responseType === 'Audio'); 244 | $endpoint = "/v1beta/models/{$params['model']}:" . $method; 245 | 246 | $response = $this->http->post($endpoint, $body); 247 | 248 | if ($method === 'predictLongRunning') { 249 | $operation = $response->json()['name']; 250 | do { 251 | sleep(5); 252 | $status = $this->http->get($operation)->json(); 253 | } while (!$status['done']); 254 | return $this->handleResponse($this->http->get("/v1beta/".$status['response']['generatedSamples'][0][$responseType === 'Video' ? 'video' : 'uri']), $responseType); 255 | } 256 | 257 | // Check for error response 258 | if (isset($response->json()['candidates'][0]['finishReason']) && $response->json()['candidates'][0]['finishReason'] != 'STOP') { 259 | Log::error('Gemini API error response', ['response' => $response->json()]); 260 | throw new ApiException("API request failed with finishReason: {$response->json()['candidates'][0]['finishReason']}"); 261 | } 262 | 263 | return $this->handleResponse($response, $responseType); 264 | } 265 | 266 | protected function buildRequestBody(array $params, bool $forLongRunning = false, bool $forAudio = false): array 267 | { 268 | $method = $params['method'] ?? 'generateContent'; 269 | $isPredict = $method === 'predict' || $method === 'predictLongRunning'; 270 | if ($isPredict) { 271 | // Structure for predict/predictLongRunning 272 | $instance = ['prompt' => $params['prompt'] ?? '']; 273 | if (isset($params['filePath']) && isset($params['fileType'])) { 274 | $filePart = $params['fileType'] === 'image' ? [ 275 | 'inlineData' => [ 276 | 'mimeType' => $this->getMimeType($params['fileType'], $params['filePath']), 277 | 'data' => base64_encode(file_get_contents($params['filePath'])) 278 | ] 279 | ] : [ 280 | 'fileData' => [ 281 | 'mimeType' => $this->getMimeType($params['fileType'], $params['filePath']), 282 | 'fileUri' => $this->upload($params['fileType'], $params['filePath']) 283 | ] 284 | ]; 285 | $instance = array_merge($instance, $filePart); 286 | } elseif (isset($params['fileUri']) && isset($params['fileType'])) { 287 | $filePart = [ 288 | 'fileData' => [ 289 | 'mimeType' => $params['fileType'], 290 | 'fileUri' => $params['fileUri'] 291 | ] 292 | ]; 293 | $instance = array_merge($instance, $filePart); 294 | } 295 | $body = [ 296 | 'instances' => [$instance], 297 | 'parameters' => [ 298 | 'temperature' => $params['temperature'] ?? 0.7, 299 | 'maxOutputTokens' => $params['maxTokens'] ?? 1024, 300 | ], 301 | ]; 302 | if (isset($params['safetySettings'])) { 303 | $body['parameters']['safetySettings'] = $params['safetySettings']; 304 | } 305 | } else { 306 | // Structure for generateContent 307 | if (!isset($params['prompt']) || empty($params['prompt'])) { 308 | throw new ValidationException('Prompt is required for audio generation (TTS).'); 309 | } 310 | $body = [ 311 | 'contents' => $params['contents'] ?? [['parts' => [['text' => $params['prompt'] ?? '']]]], 312 | 'generationConfig' => [ 313 | 'temperature' => $params['temperature'] ?? 0.7, 314 | 'maxOutputTokens' => $params['maxTokens'] ?? 1024, 315 | ], 316 | 'safetySettings' => $params['safetySettings'] ?? config('gemini.safety_settings'), 317 | ]; 318 | if (isset($params['filePath']) && isset($params['fileType'])) { 319 | $filePart = $params['fileType'] === 'image' ? [ 320 | 'inlineData' => [ 321 | 'mimeType' => $this->getMimeType($params['fileType'], $params['filePath']), 322 | 'data' => base64_encode(file_get_contents($params['filePath'])) 323 | ] 324 | ] : [ 325 | 'fileData' => [ 326 | 'mimeType' => $this->getMimeType($params['fileType'], $params['filePath']), 327 | 'fileUri' => $this->upload($params['fileType'], $params['filePath']) 328 | ] 329 | ]; 330 | $body['contents'][0]['parts'][] = $filePart; 331 | } elseif (isset($params['fileUri']) && isset($params['fileType'])) { 332 | $filePart = [ 333 | 'fileData' => [ 334 | 'mimeType' => $params['fileType'], 335 | 'fileUri' => $params['fileUri'] 336 | ] 337 | ]; 338 | $body['contents'][0]['parts'][] = $filePart; 339 | } 340 | if ($forAudio) { 341 | $body['generationConfig']['responseModalities'] = ['AUDIO']; 342 | $speechConfig = config('gemini.default_speech_config', []); 343 | if (isset($params['multiSpeaker']) && $params['multiSpeaker']) { 344 | $speechConfig['multiSpeakerVoiceConfig'] = [ 345 | 'speakerVoiceConfigs' => $params['speakerVoices'] ?? [] 346 | ]; 347 | } else { 348 | $speechConfig['voiceConfig'] = [ 349 | 'prebuiltVoiceConfig' => [ 350 | 'voiceName' => $params['voiceName'] ?? $speechConfig['voiceName'] ?? config('gemini.providers.gemini.default_speech_config.voiceName') 351 | ] 352 | ]; 353 | } 354 | $body['generationConfig']['speechConfig'] = $speechConfig; 355 | } 356 | } 357 | 358 | if (isset($params['system'])) { 359 | $body[$isPredict ? 'parameters' : 'systemInstruction'] = ['parts' => [['text' => $params['system']]]]; 360 | } 361 | 362 | if (isset($params['history'])) { 363 | $body['contents'] = [['role' => 'user', 'parts' => [['text' => $params['prompt'] ?? '']]]]; 364 | $body[$isPredict ? 'instances' : 'contents'] = array_merge($params['history'], $body[$isPredict ? 'instances' : 'contents']); 365 | } 366 | 367 | if (isset($params['functions'])) { 368 | $body[$isPredict ? 'parameters' : 'tools'] = ['functionDeclarations' => $params['functions']]; 369 | } 370 | 371 | if (isset($params['structuredSchema'])) { 372 | $body[$isPredict ? 'parameters' : 'generationConfig']['responseMimeType'] = 'application/json'; 373 | $body[$isPredict ? 'parameters' : 'generationConfig']['responseSchema'] = $params['structuredSchema']; 374 | } 375 | 376 | if ($forLongRunning) { 377 | // For predictLongRunning, no additional changes needed as per docs 378 | } 379 | return $body; 380 | } 381 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Gemini 2 | 3 | A production-ready Laravel package to integrate with the Google Gemini API. Supports text, image, video, audio, long-context, structured output, files, caching, function-calling and understanding capabilities. 4 | 5 | [![Version](https://img.shields.io/packagist/v/hosseinhezami/laravel-gemini.svg)](https://packagist.org/packages/hosseinhezami/laravel-gemini) 6 | [![Downloads](https://img.shields.io/packagist/dt/hosseinhezami/laravel-gemini.svg)](https://packagist.org/packages/hosseinhezami/laravel-gemini) 7 | [![Star](https://img.shields.io/packagist/stars/hosseinhezami/laravel-gemini.svg)](https://packagist.org/packages/hosseinhezami/laravel-gemini) 8 | [![License](https://img.shields.io/packagist/l/hosseinhezami/laravel-gemini.svg)](https://packagist.org/packages/hosseinhezami/laravel-gemini) 9 | [![Laravel Compatible](https://img.shields.io/badge/Laravel-10%2B-brightgreen.svg)](https://hosseinhezami.github.io/laravel-gemini) 10 | 11 | ## Features 12 | 13 | - 🤖 Text generation with context and history 14 | - 🖼️ Image generation and understanding 15 | - 🎥 Video generation and analysis 16 | - 🔊 Audio synthesis and transcription 17 | - 📄 Document processing and understanding 18 | - 🔍 Embeddings generation 19 | - 📊 File management capabilities 20 | - ⚡ Real-time streaming responses 21 | - 🛡️ Configurable safety settings 22 | - 🗄️ Caching for pre-processed content 23 | 24 | ## Installation 25 | 26 | ```bash 27 | composer require hosseinhezami/laravel-gemini 28 | ``` 29 | 30 | Publish the configuration file: 31 | 32 | ```bash 33 | php artisan vendor:publish --tag=gemini-config 34 | ``` 35 | 36 | Add your Gemini API key to your `.env` file: 37 | 38 | ```env 39 | GEMINI_API_KEY=your_gemini_api_key_here 40 | ``` 41 | 42 | ## Configuration (detailed) 43 | 44 | Configuration lives in `config/gemini.php`. Below are the most important keys and recommended defaults: 45 | 46 | | Key | Description | Default | 47 | |---|---:|---| 48 | | `api_key` | Your Gemini API key. | `env('GEMINI_API_KEY')` | 49 | | `base_uri` | Base API endpoint. | `https://generativelanguage.googleapis.com/v1beta/` | 50 | | `default_provider` | Which provider config to use by default. | `gemini` | 51 | | `timeout` | Request timeout in seconds. | `30` | 52 | | `retry_policy.max_retries` | Retry attempts for failed requests. | `30` | 53 | | `retry_policy.retry_delay` | Delay between retries in ms. | `1000` | 54 | | `logging` | Log requests/responses (useful for debugging). | `false` | 55 | | `stream.chunk_size` | Stream buffer chunk size. | `1024` | 56 | | `stream.timeout` | Stream timeout (ms). | `1000` | 57 | | `caching.default_ttl` | Default TTL for cache expiration (e.g., '3600s'). | `'3600s'` | 58 | | `caching.default_page_size` | Default page size for listing caches. | `50` | 59 | 60 | ### Providers / models / methods 61 | 62 | The `providers` array lets you map capability types to models and HTTP methods the provider uses: 63 | 64 | | Provider | Capability | Config key | Default model | Default method | 65 | |---|---:|---|---:|---| 66 | | `gemini` | text | `providers.gemini.models.text` | `gemini-2.5-flash-lite` | `generateContent` | 67 | | `gemini` | image | `providers.gemini.models.image` | `gemini-2.5-flash-image-preview` | `generateContent` or `predict` | 68 | | `gemini` | video | `providers.gemini.models.video` | `veo-3.0-fast-generate-001` | `predictLongRunning` | 69 | | `gemini` | audio | `providers.gemini.models.audio` | `gemini-2.5-flash-preview-tts` | `generateContent` | 70 | | `gemini` | embeddings | `providers.gemini.models.embedding` | `gemini-embedding-001` | n/a (embeddings endpoint) | 71 | 72 | **Speech config** (`providers.gemini.default_speech_config`) example: 73 | 74 | ```php 75 | 'default_speech_config' => [ 76 | 'voiceName' => 'Kore', 77 | // 'speakerVoices' => [ 78 | // ['speaker' => 'Joe', 'voiceName' => 'Kore'], 79 | // ['speaker' => 'Jane', 'voiceName' => 'Puck'], 80 | // ], 81 | ], 82 | ``` 83 | 84 | ## Dynamic API Key Configuration 85 | 86 | By default, Laravel Gemini reads the API key from your `.env` file (`GEMINI_API_KEY`). 87 | 88 | However, you can now **set the API key dynamically at runtime** using the new `setApiKey()` method. 89 | This is useful when you want to switch between multiple keys (e.g. per-user or per-request). 90 | 91 | **Example:** 92 | 93 | ```php 94 | use HosseinHezami\LaravelGemini\Facades\Gemini; 95 | 96 | // Dynamically set API key (takes priority over .env) 97 | Gemini::setApiKey('my-custom-api-key'); 98 | 99 | // Use Gemini as usual 100 | $response = Gemini::text() 101 | ->prompt('Hello Gemini!') 102 | ->generate(); 103 | 104 | echo $response->content(); 105 | ```` 106 | 107 | If `setApiKey()` is not called, the package will automatically use the default key from `.env`. 108 | 109 | **ApiKey priority order:** 110 | 111 | 1. Manually set key via `Gemini::setApiKey()` 112 | 2. Config value (`config/gemini.php`) 113 | 3. `.env` variable (`GEMINI_API_KEY`) 114 | 115 | --- 116 | 117 | ## Builder APIs — full method reference 118 | 119 | This package exposes a set of builder-style facades: `Gemini::text()`, `Gemini::image()`, `Gemini::video()`, `Gemini::audio()`, `Gemini::embeddings()`, `Gemini::files()` and `Gemini::caches()`. 120 | 121 | Below is a concise reference of commonly available chainable methods and what they do. Method availability depends on the builder. 122 | 123 | ### Common response helpers (Response object) 124 | When you call `->generate()` (or a polling save on long-running jobs) you typically get a response object with these helpers: 125 | 126 | - `content()` — main textual output (string). 127 | - `model()` — model name used. 128 | - `usage()` — usage / billing info returned by the provider. 129 | - `requestId()` — provider request id. 130 | - `save($path)` — convenience method to download and persist a result to disk (media). 131 | 132 | --- 133 | 134 | ### Gemini:: 135 | 136 | ```php 137 | use HosseinHezami\LaravelGemini\Facades\Gemini; 138 | ``` 139 | 140 | ### TextBuilder (`Gemini::text()`) 141 | 142 | Use for: chat-like generation, long-context text, structured output, and multimodal understanding (text responses after uploading files). 143 | 144 | Common methods: 145 | 146 | | Method | Args | Description | 147 | |---|---:|---| 148 | | `model(string)` | model id | Choose model to use. | 149 | | `prompt(string/array)` | user prompt or parts | Main prompt(s). | 150 | | `system(string)` | system instruction | System-level instruction. | 151 | | `history(array)` | chat history | Conversation history array (role/parts structure). | 152 | | `structuredSchema(array)` | JSON Schema | Ask model to produce structured JSON (schema validation). | 153 | | `temperature(float)` | 0.0-1.0 | Sampling temperature. | 154 | | `maxTokens(int)` | token limit | Max tokens for generation. | 155 | | `safetySettings(array)` | array | Safety thresholds from config. | 156 | | `method(string)` | provider method | Override provider method name (e.g., `generateContent`). | 157 | | `upload(string $type, string $path)` | (type, local-file-path) | Attach a file (image/document/audio/video) to the request. | 158 | | `cache(array $tools = [], array $toolConfig = [], string $displayName = null, string $ttl = null, string $expireTime = null)` | optional params | Create a cache from current builder params and return cache name. | 159 | | `getCache(string $name)` | cache name | Get details of a cached content. | 160 | | `cachedContent(string $name)` | cache name | Use a cached content for generation. | 161 | | `stream(callable)` | callback | Stream chunks (SSE / server events). | 162 | | `generate()` | — | Execute request and return a Response object. | 163 | 164 | **Notes on `history` structure** 165 | History entries follow a `role` + `parts` format: 166 | 167 | ```php 168 | [ 169 | ['role' => 'user', 'parts' => [['text' => 'User message']]], 170 | ['role' => 'model', 'parts' => [['text' => 'Assistant reply']]] 171 | ] 172 | ``` 173 | 174 | **Text** 175 | 176 | ```php 177 | $response = Gemini::text() 178 | ->model('gemini-2.5-flash') 179 | ->system('You are a helpful assistant.') 180 | ->prompt('Write a conversation between human and Ai') 181 | ->history([ 182 | ['role' => 'user', 'parts' => [['text' => 'Hello AI']]], 183 | ['role' => 'model', 'parts' => [['text' => 'Hello human!']]] 184 | ]) 185 | ->temperature( 0.7) 186 | ->maxTokens(1024) 187 | ->generate(); 188 | 189 | echo $response->content(); 190 | ``` 191 | 192 | **Streaming Responses** 193 | 194 | ```php 195 | return response()->stream(function () use ($request) { 196 | Gemini::text() 197 | ->model('gemini-2.5-flash') 198 | ->prompt('Tell a long story about artificial intelligence.') 199 | ->stream(function ($chunk) { 200 | $text = $chunk['text'] ?? ''; 201 | if (!empty(trim($text))) { 202 | echo "data: " . json_encode(['text' => $text]) . "\n\n"; 203 | ob_flush(); 204 | flush(); 205 | } 206 | }); 207 | }, 200, [ 208 | 'Content-Type' => 'text/event-stream', 209 | 'Cache-Control' => 'no-cache', 210 | 'Connection' => 'keep-alive', 211 | 'X-Accel-Buffering' => 'no', 212 | ]); 213 | ``` 214 | 215 | **Document Understanding** 216 | 217 | ```php 218 | $response = Gemini::text() 219 | ->upload('document', $filePath) // image, video, audio, document 220 | ->prompt('Extract the key points from this document.') 221 | ->generate(); 222 | 223 | echo $response->content(); 224 | ``` 225 | 226 | **Structured output** 227 | 228 | ```php 229 | $response = Gemini::text() 230 | ->model('gemini-2.5-flash') 231 | ->structuredSchema([ 232 | 'type' => 'object', 233 | 'properties' => [ 234 | 'name' => ['type' => 'string'], 235 | 'age' => ['type' => 'integer'] 236 | ], 237 | 'required' => ['name'] 238 | ]) 239 | ->prompt('Return a JSON object with name and age.') 240 | ->generate(); 241 | 242 | $json = $response->content(); // Parsable JSON matching the schema 243 | ``` 244 | 245 | --- 246 | 247 | ### ImageBuilder (`Gemini::image()`) 248 | 249 | Use for image generation. 250 | 251 | | Method | Args | Description | 252 | |---|---:|---| 253 | | `model(string)` | model id | Model for image generation. | 254 | | `prompt(string)` | prompt text | Image description. | 255 | | `method(string)` | e.g. `predict` | Provider method (predict / generateContent). | 256 | | `cache(array $tools = [], array $toolConfig = [], string $displayName = null, string $ttl = null, string $expireTime = null)` | optional params | Create a cache from current builder params and return cache name. | 257 | | `getCache(string $name)` | cache name | Get details of a cached content. | 258 | | `cachedContent(string $name)` | cache name | Use a cached content for generation. | 259 | | `generate()` | — | Run generation. | 260 | | `save($path)` | local path | Save image bytes to disk. | 261 | 262 | **Image** 263 | 264 | ```php 265 | $response = Gemini::image() 266 | ->model('gemini-2.5-flash-image-preview') 267 | ->method('generateContent') 268 | ->prompt('A futuristic city skyline at sunset.') 269 | ->generate(); 270 | 271 | $response->save('image.png'); 272 | ``` 273 | 274 | --- 275 | 276 | ### VideoBuilder (`Gemini::video()`) 277 | 278 | Use for short or long-running video generation. 279 | 280 | | Method | Args | Description | 281 | |---|---:|---| 282 | | `model(string)` | model id | Video model. | 283 | | `prompt(string)` | prompt | Describe the video. | 284 | | `cache(array $tools = [], array $toolConfig = [], string $displayName = null, string $ttl = null, string $expireTime = null)` | optional params | Create a cache from current builder params and return cache name. | 285 | | `getCache(string $name)` | cache name | Get details of a cached content. | 286 | | `cachedContent(string $name)` | cache name | Use a cached content for generation. | 287 | | `generate()` | — | Initiates video creation (may be long-running). | 288 | | `save($path)` | local path | Polls provider and saves final video file. | 289 | 290 | **Note:** long-running video generation typically uses `predictLongRunning` or similar. The package abstracts polling & saving. 291 | 292 | --- 293 | 294 | ### AudioBuilder (`Gemini::audio()`) 295 | 296 | Use for TTS generation. 297 | 298 | | Method | Args | Description | 299 | |---|---:|---| 300 | | `model(string)` | model id | TTS model. | 301 | | `prompt(string)` | text-to-speak | Audio file description | 302 | | `voiceName(string)` | voice id | Select a voice (e.g. `Kore`). | 303 | | `speakerVoices(array)` | speakers array | Speakers (e.g. [['speaker' => 'Joe', 'voiceName' => 'Kore'], ['speaker' => 'Jane', 'voiceName' => 'Puck']]). | 304 | | `cache(array $tools = [], array $toolConfig = [], string $displayName = null, string $ttl = null, string $expireTime = null)` | optional params | Create a cache from current builder params and return cache name. | 305 | | `getCache(string $name)` | cache name | Get details of a cached content. | 306 | | `cachedContent(string $name)` | cache name | Use a cached content for generation. | 307 | | `generate()` | — | Generate audio bytes. | 308 | | `save($path)` | local path | Save generated audio (wav/mp3). | 309 | 310 | --- 311 | 312 | ### Embeddings (`Gemini::embeddings()`) 313 | 314 | Accepts a payload array. Typical shape: 315 | 316 | ```php 317 | $embeddings = Gemini::embeddings([ 318 | 'model' => 'gemini-embedding-001', 319 | 'content' => ['parts' => [['text' => 'Text to embed']]], 320 | ]); 321 | 322 | /* embedding_config */ 323 | // https://ai.google.dev/gemini-api/docs/embeddings 324 | // 'embedding_config': { 325 | // 'embedding_config': { 326 | // 'task_type': 'SEMANTIC_SIMILARITY', // SEMANTIC_SIMILARITY, CLASSIFICATION, CLUSTERING, RETRIEVAL_DOCUMENT, RETRIEVAL_QUERY, CODE_RETRIEVAL_QUERY, QUESTION_ANSWERING, FACT_VERIFICATION 327 | // 'embedding_dimensionality': 768 // 128, 256, 512, 768, 1536, 2048 328 | // } 329 | // } 330 | ``` 331 | Return value is the raw embeddings structure (provider-specific). Use these vectors for semantic search, similarity, clustering, etc. 332 | 333 | --- 334 | 335 | ### Files API (`Gemini::files()`) 336 | 337 | High level file manager for uploads used by the "understanding" endpoints. 338 | 339 | | Method | Args | Description | 340 | |---|---:|---| 341 | | `upload(string $type, string $localPath)` | `type` in `[document,image,video,audio]` | Upload a local file and return a provider `uri` or `file id`. | 342 | | `list()` | — | Return a list of uploaded files (metadata). | 343 | | `get(string $id)` | file id | Get file metadata (name, uri, state, mimeType, displayName). | 344 | | `delete(string $id)` | file id | Delete a previously uploaded file. | 345 | 346 | **Files** 347 | 348 | ```php 349 | // Upload a file 350 | $uri = Gemini::files()->upload('document', $pathToFile); 351 | 352 | // List all files 353 | $files = Gemini::files()->list(); 354 | 355 | // Get file details 356 | $fileInfo = Gemini::files()->get($file_id); 357 | 358 | // Delete a file 359 | $success = Gemini::files()->delete($file_id); 360 | ``` 361 | 362 | **Supported file types & MIME** 363 | 364 | | Category | Extension | MIME type | 365 | |---|---:|---| 366 | | image | png | image/png | 367 | | image | jpeg | image/jpeg | 368 | | image | jpg | image/jpeg | 369 | | image | webp | image/webp | 370 | | image | heic | image/heic | 371 | | image | heif | image/heif | 372 | | video | mp4 | video/mp4 | 373 | | video | mpeg | video/mpeg | 374 | | video | mov | video/mov | 375 | | video | avi | video/avi | 376 | | video | flv | video/x-flv | 377 | | video | mpg | video/mpg | 378 | | video | webm | video/webm | 379 | | video | wmv | video/wmv | 380 | | video | 3gpp | video/3gpp | 381 | | audio | wav | audio/wav | 382 | | audio | mp3 | audio/mp3 | 383 | | audio | aiff | audio/aiff | 384 | | audio | aac | audio/aac | 385 | | audio | ogg | audio/ogg | 386 | | audio | flac | audio/flac | 387 | | document | pdf | application/pdf | 388 | | document | txt | text/plain | 389 | | document | md | text/markdown | 390 | 391 | --- 392 | 393 | ### Caching API (`Gemini::caches()`) 394 | 395 | High-level cache manager for pre-processing and storing content (prompts, system instructions, history, files) to reuse in generation requests, reducing latency and costs. Caches are model-specific and temporary. 396 | 397 | | Method | Args | Description | 398 | |---|---:|---| 399 | | `create(string $model, array $contents, ?string $systemInstruction = null, array $tools = [], array $toolConfig = [], ?string $displayName = null, ?string $ttl = null, ?string $expireTime = null)` | required/optional params | Create a cached content and return CacheResponse. | 400 | | `list(?int $pageSize = null, ?string $pageToken = null)` | optional params | List cached contents (supports pagination). | 401 | | `get(string $name)` | cache name | Get details of a cached content. | 402 | | `update(string $name, ?string $ttl = null, ?string $expireTime = null)` | cache name and expiration | Update cache expiration (TTL or expireTime). | 403 | | `delete(string $name)` | cache name | Delete a cached content. | 404 | 405 | **Caching** 406 | 407 | ```php 408 | // Create a cache 409 | $cache = Gemini::caches()->create( 410 | model: 'gemini-2.5-flash', 411 | contents: [['role' => 'user', 'parts' => [['text' => 'Sample content']]]], 412 | systemInstruction: 'You are a helpful assistant.', 413 | tools: [], // Optional 414 | toolConfig: [], // Optional 415 | displayName: 'My Cache', // Optional 416 | ttl: '600s' // Optional TTL (e.g., '300s') or expireTime: '2024-12-31T23:59:59Z' 417 | ); 418 | $cacheName = $cache->name(); // e.g., 'cachedContents/abc123' 419 | 420 | // List all caches 421 | $caches = Gemini::caches()->list(pageSize: 50, pageToken: 'nextPageToken'); 422 | 423 | // Get cache details 424 | $cacheInfo = Gemini::caches()->get($cacheName); 425 | 426 | // Update cache expiration 427 | $updatedCache = Gemini::caches()->update( 428 | name: $cacheName, 429 | ttl: '1200s' // Or expireTime: '2024-12-31T23:59:59Z' 430 | ); 431 | 432 | // Delete a cache 433 | $success = Gemini::caches()->delete($cacheName); 434 | ``` 435 | 436 | **CacheResponse Methods** 437 | 438 | - `name()`: Returns the cache name (e.g., 'cachedContents/abc123') 439 | - `displayName()`: Returns the Display Name (e.g., 'Default Cache') 440 | - `model()`: Returns the model used 441 | - `expireTime()`: Returns expiration 442 | - `usageMetadata()`: Returns usage metadata 443 | - `toArray()`: Full response as array 444 | 445 | **Caching in Generation Builders** 446 | 447 | Caching is also integrated into text, image, video, and audio builders for seamless use: 448 | 449 | ```php 450 | // Create cache from builder params 451 | $cacheName = Gemini::text() 452 | ->model('gemini-2.5-flash') 453 | ->prompt('Sample prompt') 454 | ->system('System instruction') 455 | ->history([['role' => 'user', 'parts' => [['text' => 'History item']]]]) // optional 456 | ->cache( 457 | tools: [], // optional 458 | toolConfig: [], // optional 459 | displayName: 'My Cache', // optional 460 | ttl: '600s' // optional, or expireTime 461 | ); 462 | 463 | // Get cache details from builder 464 | $cacheInfo = Gemini::text()->getCache($cacheName); 465 | 466 | // Use cached content in generation 467 | $response = Gemini::text() 468 | ->prompt('Summarize this.') 469 | ->cachedContent($cacheName) 470 | ->generate(); 471 | ``` 472 | 473 | For more details, refer to the [Gemini API Caching Documentation](https://ai.google.dev/api/caching). 474 | 475 | --- 476 | 477 | ## Streaming (Server-Sent Events) 478 | The `stream` route uses `Content-Type: text/event-stream`. Connect from a browser or SSE client and consume `data: ` messages per chunk. 479 | 480 | --- 481 | 482 | ### Streaming behaviour 483 | 484 | - Implemented using SSE (Server-Sent Events). The stream yields chunks where each chunk is typically `['text' => '...']`. 485 | - Client should reconnect behaviorally for resilience and handle partial chunks. 486 | - Use response headers: 487 | - `Content-Type: text/event-stream` 488 | - `Cache-Control: no-cache` 489 | - `Connection: keep-alive` 490 | - `X-Accel-Buffering: no` 491 | 492 | --- 493 | 494 | ## Tips, error handling & best practices 495 | 496 | - Respect provider limits — pick appropriate `maxTokens` and `temperature`. 497 | - For large media (video) prefer long-running `predictLongRunning` models and rely on `save()` to poll and download final asset. 498 | - Use `safetySettings` from config for content filtering. You can override per-request. 499 | - When uploading user-supplied files, validate MIME type and size before calling `Gemini::files()->upload`. 500 | - For caching, use TTL wisely to avoid expired caches; always check expiration in responses. 501 | 502 | --- 503 | 504 | ## Artisan Commands 505 | 506 | The package includes helpful Artisan commands: 507 | 508 | | Command | Description | 509 | |------------------------------|-----------------------------| 510 | | `php artisan gemini:models` | List available models. | 511 | 512 | --- 513 | 514 | ## Contributing 515 | 516 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 517 | 518 | ## License 519 | 520 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 521 | --------------------------------------------------------------------------------