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
32 changes: 16 additions & 16 deletions .github/model-watch.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

declare(strict_types=1);

use PapiAI\Google\GeminiModel;

/*
* Compares the model IDs this package ships against what the provider currently serves.
*
Expand All @@ -23,6 +25,8 @@
* them on purpose so callers get a deprecation note rather than an undefined-constant fatal.
*/

require __DIR__ . '/../vendor/autoload.php';

$source = file_get_contents(__DIR__ . '/../src/GoogleProvider.php');

if ($source === false) {
Expand All @@ -33,28 +37,24 @@

/** @var array<string, array{value: string, deprecated: bool, isDefault: bool}> $shipped */
$shipped = [];
$lines = explode("\n", $source);

// Line by line, not one big regex. A regex that also tries to capture the preceding docblock
// matches across earlier constants and silently drops them, and a checker that quietly skips
// models is worse than no checker: the ones it skipped here were the newest four.
foreach ($lines as $number => $line) {
if (preg_match('/public const (?P<name>MODEL_\w+|IMAGEN_\w+) = \'(?P<value>[^\']+)\';/', $line, $match) !== 1) {
continue;
}

// Only the line immediately above can carry the marker, which is where php-cs-fixer puts it.
$shipped[$match['name']] = [
'value' => $match['value'],
'deprecated' => str_contains($lines[$number - 1] ?? '', '@deprecated'),
// The enum is the source of truth: every ID we ship is a case, and each case knows whether it
// has been retired. No parsing of docblocks, which is what silently skipped four models before.
foreach (GeminiModel::cases() as $model) {
$shipped[$model->name] = [
'value' => $model->value,
'deprecated' => $model->isDeprecated(),
'isDefault' => false,
];
}

// Anything used as a fallback is the dangerous kind: it breaks callers who pass no model at all.
foreach ($shipped as $name => $entry) {
if (preg_match('/\?\?\s*self::' . preg_quote($name, '/') . '\b/', $source) === 1) {
$shipped[$name]['isDefault'] = true;
// The provider spells a default as `?? self::CONSTANT`, and each constant aliases an enum case.
preg_match_all('/public const (?P<constant>\w+) = GeminiModel::(?P<case>\w+)->value;/', $source, $aliases, PREG_SET_ORDER);

foreach ($aliases as $alias) {
if (isset($shipped[$alias['case']]) && preg_match('/\?\?\s*self::' . preg_quote($alias['constant'], '/') . '\b/', $source) === 1) {
$shipped[$alias['case']]['isDefault'] = true;
}
}

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/model-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
php-version: '8.3'
coverage: none

- name: Install dependencies
run: composer install --no-dev --prefer-dist --no-progress --no-interaction

- name: Check models
id: check
run: php .github/model-watch.php
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ use PapiAI\Google\GoogleProvider;

$provider = new GoogleProvider(
apiKey: $_ENV['GOOGLE_API_KEY'],
defaultModel: GoogleProvider::MODEL_3_6_FLASH,
defaultModel: GoogleProvider::MODEL_3_8_FLASH,
);

$agent = new Agent(
provider: $provider,
model: GoogleProvider::MODEL_3_6_FLASH,
model: GoogleProvider::MODEL_3_8_FLASH,
instructions: 'You are a helpful assistant.',
);

Expand All @@ -36,7 +36,9 @@ echo $response->text;
### Gemini (text and chat)

```php
GoogleProvider::MODEL_3_6_FLASH // 'gemini-3.6-flash' (default)
GoogleProvider::MODEL_3_8_FLASH // 'gemini-3.8-flash' (default)
GoogleProvider::MODEL_3_7_FLASH // 'gemini-3.7-flash'
GoogleProvider::MODEL_3_6_FLASH // 'gemini-3.6-flash'
GoogleProvider::MODEL_3_5_FLASH // 'gemini-3.5-flash'
GoogleProvider::MODEL_3_5_FLASH_LITE // 'gemini-3.5-flash-lite'
GoogleProvider::MODEL_3_1_PRO // 'gemini-3.1-pro-preview'
Expand Down
8 changes: 5 additions & 3 deletions docs/provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ use PapiAI\Google\GoogleProvider;

$provider = new GoogleProvider(
apiKey: $_ENV['GOOGLE_API_KEY'],
defaultModel: GoogleProvider::MODEL_3_6_FLASH,
defaultModel: GoogleProvider::MODEL_3_8_FLASH,
);

$agent = new Agent(
provider: $provider,
model: GoogleProvider::MODEL_3_6_FLASH,
model: GoogleProvider::MODEL_3_8_FLASH,
instructions: 'You are a helpful assistant.',
);

Expand All @@ -34,7 +34,9 @@ echo $response->text;
### Chat Models

```php
GoogleProvider::MODEL_3_6_FLASH // gemini-3.6-flash (default)
GoogleProvider::MODEL_3_8_FLASH // gemini-3.8-flash (default)
GoogleProvider::MODEL_3_7_FLASH // gemini-3.7-flash
GoogleProvider::MODEL_3_6_FLASH // gemini-3.6-flash
GoogleProvider::MODEL_3_5_FLASH // gemini-3.5-flash
GoogleProvider::MODEL_3_5_FLASH_LITE // gemini-3.5-flash-lite
GoogleProvider::MODEL_3_1_PRO // gemini-3.1-pro-preview
Expand Down
161 changes: 161 additions & 0 deletions src/GeminiModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

/*
* This file is part of PapiAI,
* A simple but powerful PHP library for building AI agents.
*
* (c) Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace PapiAI\Google;

use PapiAI\Core\Effort;

/**
* Every Google model this package knows, and what each one accepts.
*
* The model, not the provider, decides which thinking knob it takes, whether thinking can be
* switched off and which effort levels exist. Putting that here keeps the provider free of
* string-sniffing, and lets the model watch enumerate what we ship instead of parsing source.
*
* An ID we have not heard of is not an error: `tryFrom()` returns null and the provider reads the
* generation from the name, assuming newer rather than older.
*
* Retirement dates are Google's published shutdown dates, ISO formatted.
*
* @see https://ai.google.dev/gemini-api/docs/models
*/
enum GeminiModel: string
{
case Flash38 = 'gemini-3.8-flash';
case Flash37 = 'gemini-3.7-flash';
case Flash36 = 'gemini-3.6-flash';
case Flash35 = 'gemini-3.5-flash';
case Flash35Lite = 'gemini-3.5-flash-lite';
case Pro31 = 'gemini-3.1-pro-preview';
case Flash31Lite = 'gemini-3.1-flash-lite';
case Flash3 = 'gemini-3-flash-preview';
case Pro25 = 'gemini-2.5-pro';
case Flash25 = 'gemini-2.5-flash';
case Flash25Lite = 'gemini-2.5-flash-lite';
case Flash20 = 'gemini-2.0-flash';
case Flash20Lite = 'gemini-2.0-flash-lite';
/** @deprecated Shut down 9 March 2026; the alias now redirects to gemini-3.1-pro-preview. */
case Pro3 = 'gemini-3-pro-preview';
/** @deprecated Retired; no longer published by Google. */
case Pro15 = 'gemini-1.5-pro';
/** @deprecated Retired; no longer published by Google. */
case Flash15 = 'gemini-1.5-flash';

case Flash31Image = 'gemini-3.1-flash-image';
case Flash31LiteImage = 'gemini-3.1-flash-lite-image';
case Pro3Image = 'gemini-3-pro-image';
case Flash25Image = 'gemini-2.5-flash-image';
/** @deprecated Imagen shut down 17 August 2026. Use Flash31Image. */
case Imagen4 = 'imagen-4.0-generate-001';
/** @deprecated Imagen shut down 17 August 2026. Use Flash31Image. */
case Imagen4Ultra = 'imagen-4.0-ultra-generate-001';
/** @deprecated Imagen shut down 17 August 2026. Use Flash31Image. */
case Imagen4Fast = 'imagen-4.0-fast-generate-001';
/** @deprecated Imagen 3 is already shut down. Use Flash31Image. */
case ImagenEdit = 'imagen-3.0-capability-001';

case Veo31 = 'veo-3.1-generate-preview';
case Veo31Lite = 'veo-3.1-lite-generate-preview';
/** @deprecated Shut down 30 June 2026; requests fail. Use Veo31. */
case Veo3 = 'veo-3.0-generate-001';
/** @deprecated Shut down 30 June 2026; requests fail. Use Veo31. */
case Veo2 = 'veo-2.0-generate-001';

/**
* Whether this model takes a thinking level (Gemini 3 and later) rather than a token budget.
*
* Google warns that the budget knob behaves unpredictably on Gemini 3 Pro, so the level knob
* is always used from 3 on.
*/
public function takesThinkingLevel(): bool
{
return match ($this) {
self::Flash38, self::Flash37, self::Flash36, self::Flash35, self::Flash35Lite,
self::Pro31, self::Flash31Lite, self::Flash3, self::Pro3 => true,
default => false,
};
}

/**
* The thinking levels this model accepts, in the neutral vocabulary.
*
* Only meaningful for models that take a level. No Gemini 3 model can switch thinking off,
* and Pro does not accept MINIMAL at all, so its floor is LOW. Budget models return an empty
* list: their knob is continuous.
*
* @return list<Effort>
*/
public function effortLevels(): array
{
if (!$this->takesThinkingLevel()) {
return [];
}

return match ($this) {
self::Pro31, self::Pro3 => [Effort::Low, Effort::Medium, Effort::High],
default => [Effort::Minimal, Effort::Low, Effort::Medium, Effort::High],
};
}

/**
* Whether a zero budget genuinely disables thinking. Only the pre-3 Flash families can;
* Pro has a floor it will not go below.
*/
public function canDisableThinking(): bool
{
return match ($this) {
self::Flash25, self::Flash25Lite, self::Flash20, self::Flash20Lite, self::Flash15 => true,
default => false,
};
}

/**
* Whether Google has retired this model.
*/
public function isDeprecated(): bool
{
return match ($this) {
self::Pro3, self::Pro15, self::Flash15,
self::Imagen4, self::Imagen4Ultra, self::Imagen4Fast, self::ImagenEdit,
self::Veo3, self::Veo2 => true,
default => false,
};
}

/**
* The published shutdown date, ISO formatted, where Google gave one.
*/
public function retiredOn(): ?string
{
return match ($this) {
self::Pro3 => '2026-03-09',
self::Imagen4, self::Imagen4Ultra, self::Imagen4Fast => '2026-08-17',
self::Veo3, self::Veo2 => '2026-06-30',
default => null,
};
}

/**
* What to use instead, for the retired models that have a successor here.
*/
public function replacement(): ?self
{
return match ($this) {
self::Pro3 => self::Pro31,
self::Imagen4, self::Imagen4Ultra, self::Imagen4Fast, self::ImagenEdit => self::Flash31Image,
self::Veo3, self::Veo2 => self::Veo31,
default => null,
};
}
}
66 changes: 38 additions & 28 deletions src/GoogleProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,46 +85,49 @@ class GoogleProvider implements ProviderInterface, ImageProviderInterface, Embed
private const MAX_THINKING_BUDGET = 32768;

// Gemini model aliases
public const MODEL_3_6_FLASH = 'gemini-3.6-flash';
public const MODEL_3_5_FLASH = 'gemini-3.5-flash';
public const MODEL_3_5_FLASH_LITE = 'gemini-3.5-flash-lite';
public const MODEL_3_1_PRO = 'gemini-3.1-pro-preview';
public const MODEL_3_8_FLASH = GeminiModel::Flash38->value;
public const MODEL_3_7_FLASH = GeminiModel::Flash37->value;
public const MODEL_3_6_FLASH = GeminiModel::Flash36->value;
public const MODEL_3_5_FLASH = GeminiModel::Flash35->value;
public const MODEL_3_5_FLASH_LITE = GeminiModel::Flash35Lite->value;
public const MODEL_3_1_PRO = GeminiModel::Pro31->value;
/** @deprecated Shut down 9 March 2026; the alias now redirects to gemini-3.1-pro-preview. */
public const MODEL_3_0_PRO = 'gemini-3-pro-preview';
public const MODEL_3_FLASH = 'gemini-3-flash-preview';
public const MODEL_3_PRO_IMAGE = 'gemini-3-pro-image';
public const MODEL_2_5_PRO = 'gemini-2.5-pro';
public const MODEL_2_5_FLASH = 'gemini-2.5-flash';
public const MODEL_2_5_FLASH_LITE = 'gemini-2.5-flash-lite';
public const MODEL_2_0_FLASH = 'gemini-2.0-flash';
public const MODEL_2_0_FLASH_LITE = 'gemini-2.0-flash-lite';
public const MODEL_3_0_PRO = GeminiModel::Pro3->value;
public const MODEL_3_1_FLASH_LITE = GeminiModel::Flash31Lite->value;
public const MODEL_3_FLASH = GeminiModel::Flash3->value;
public const MODEL_3_PRO_IMAGE = GeminiModel::Pro3Image->value;
public const MODEL_2_5_PRO = GeminiModel::Pro25->value;
public const MODEL_2_5_FLASH = GeminiModel::Flash25->value;
public const MODEL_2_5_FLASH_LITE = GeminiModel::Flash25Lite->value;
public const MODEL_2_0_FLASH = GeminiModel::Flash20->value;
public const MODEL_2_0_FLASH_LITE = GeminiModel::Flash20Lite->value;
/** @deprecated Retired; no longer published by Google. */
public const MODEL_1_5_PRO = 'gemini-1.5-pro';
public const MODEL_1_5_PRO = GeminiModel::Pro15->value;
/** @deprecated Retired; no longer published by Google. */
public const MODEL_1_5_FLASH = 'gemini-1.5-flash';
public const MODEL_1_5_FLASH = GeminiModel::Flash15->value;

// Image generation and editing, all reached through generateContent. Imagen is retired as a
// product line and its separate predict endpoint went with it; these replace both.
public const MODEL_3_1_FLASH_IMAGE = 'gemini-3.1-flash-image';
public const MODEL_3_1_FLASH_LITE_IMAGE = 'gemini-3.1-flash-lite-image';
public const MODEL_2_5_FLASH_IMAGE = 'gemini-2.5-flash-image';
public const MODEL_3_1_FLASH_IMAGE = GeminiModel::Flash31Image->value;
public const MODEL_3_1_FLASH_LITE_IMAGE = GeminiModel::Flash31LiteImage->value;
public const MODEL_2_5_FLASH_IMAGE = GeminiModel::Flash25Image->value;

/** @deprecated Imagen shuts down 17 August 2026. Use MODEL_3_1_FLASH_IMAGE. */
public const IMAGEN_4 = 'imagen-4.0-generate-001';
public const IMAGEN_4 = GeminiModel::Imagen4->value;
/** @deprecated Imagen shuts down 17 August 2026. Use MODEL_3_1_FLASH_IMAGE. */
public const IMAGEN_4_ULTRA = 'imagen-4.0-ultra-generate-001';
public const IMAGEN_4_ULTRA = GeminiModel::Imagen4Ultra->value;
/** @deprecated Imagen shuts down 17 August 2026. Use MODEL_3_1_FLASH_IMAGE. */
public const IMAGEN_4_FAST = 'imagen-4.0-fast-generate-001';
public const IMAGEN_4_FAST = GeminiModel::Imagen4Fast->value;
/** @deprecated Imagen 3 is already shut down. Use MODEL_3_1_FLASH_IMAGE. */
public const IMAGEN_EDIT = 'imagen-3.0-capability-001';
public const IMAGEN_EDIT = GeminiModel::ImagenEdit->value;

// Veo model aliases for video generation
public const MODEL_VEO_3_1 = 'veo-3.1-generate-preview';
public const MODEL_VEO_3_1_LITE = 'veo-3.1-lite-generate-preview';
public const MODEL_VEO_3_1 = GeminiModel::Veo31->value;
public const MODEL_VEO_3_1_LITE = GeminiModel::Veo31Lite->value;
/** @deprecated Shut down 30 June 2026; requests fail. Use MODEL_VEO_3_1. */
public const MODEL_VEO_3 = 'veo-3.0-generate-001';
public const MODEL_VEO_3 = GeminiModel::Veo3->value;
/** @deprecated Shut down 30 June 2026; requests fail. Use MODEL_VEO_3_1. */
public const MODEL_VEO_2 = 'veo-2.0-generate-001';
public const MODEL_VEO_2 = GeminiModel::Veo2->value;

/** @var array<string, string> tool call ID to thought signature mapping for multi-turn tool use */
private array $thoughtSignatures = [];
Expand All @@ -139,7 +142,7 @@ class GoogleProvider implements ProviderInterface, ImageProviderInterface, Embed
*/
public function __construct(
private readonly string $apiKey,
private readonly string $defaultModel = self::MODEL_3_6_FLASH,
private readonly string $defaultModel = self::MODEL_3_8_FLASH,
private readonly int $defaultMaxTokens = 8192,
private readonly ?Effort $defaultEffort = null,
) {
Expand Down Expand Up @@ -1133,6 +1136,12 @@ private function thinkingConfigFor(Effort $effort, string $model, int $maxTokens
*/
private function levelsFor(string $model): array
{
$known = GeminiModel::tryFrom($model)?->effortLevels() ?? [];

if ($known !== []) {
return $known;
}

if (stripos($model, 'pro') !== false) {
return [Effort::Low, Effort::Medium, Effort::High];
}
Expand All @@ -1149,7 +1158,7 @@ private function levelsFor(string $model): array
*/
private function budgetFor(Effort $effort, string $model, int $maxTokens): int
{
$canDisable = stripos($model, 'flash') !== false;
$canDisable = GeminiModel::tryFrom($model)?->canDisableThinking() ?? stripos($model, 'flash') !== false;

if (!$effort->thinks() && $canDisable) {
return 0;
Expand All @@ -1167,7 +1176,8 @@ private function budgetFor(Effort $effort, string $model, int $maxTokens): int
*/
private function takesThinkingLevel(string $model): bool
{
return preg_match('/gemini-([3-9]|\d{2,})/i', $model) === 1;
return GeminiModel::tryFrom($model)?->takesThinkingLevel()
?? preg_match('/gemini-([3-9]|\d{2,})/i', $model) === 1;
}

/**
Expand Down
Loading
Loading