PluginProbe
Auto Alt Text / 3.0.3
Auto Alt Text v3.0.3
3.0.3 2.8.2 1.3.1 1.3.2 2.0.0 2.1.0 2.1.1 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.7.0 2.8.0 2.8.1 All 28 releases
auto-alt-text / src / App / AIProviders / Gemini / GeminiModelsRegistry.php

GeminiModelsRegistry.php in Auto Alt Text 3.0.3, at src/App/AIProviders/Gemini/GeminiModelsRegistry.php

264 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace AATXT\App\AIProviders\Gemini;
6
7 use AATXT\App\Infrastructure\Cache\CacheInterface;
8 use AATXT\App\Infrastructure\Http\HttpClientInterface;
9 use AATXT\Config\Constants;
10 use Exception;
11
12 /**
13 * Registry that exposes the list of Gemini models currently available,
14 * fetched from the public /v1beta/models endpoint and cached via the
15 * injected cache backend.
16 *
17 * The endpoint returns every model exposed by the Gemini API (embeddings,
18 * TTS, image generation, ...), so the list is filtered down to the
19 * vision-capable "gemini-*" text generation models, excluding special-purpose
20 * variants, experimental builds and preview snapshots.
21 *
22 * On any failure (missing API key, HTTP error, empty/malformed response) it
23 * falls back to the static list in Constants so the plugin keeps working.
24 */
25 class GeminiModelsRegistry
26 {
27 /**
28 * Cache TTL for a successful response (24 hours).
29 */
30 private const CACHE_TTL_SUCCESS = 86400;
31
32 /**
33 * Cache TTL applied to the fallback list after a fetch failure (1 hour),
34 * so we do not hammer the API while Google is unreachable.
35 */
36 private const CACHE_TTL_FALLBACK = 3600;
37
38 /**
39 * Vision-capable Gemini text generation model families.
40 */
41 private const INCLUDED_FAMILIES_PATTERN = '/^gemini-\d/i';
42
43 /**
44 * Special-purpose variants that cannot generate alt text from an image
45 * (embeddings, TTS, audio/live dialog, image generation), plus
46 * experimental and preview builds.
47 */
48 private const EXCLUDED_VARIANTS_PATTERN = '/(embedding|image|imagen|tts|audio|live|dialog|thinking|exp|preview)/i';
49
50 /**
51 * @var HttpClientInterface
52 */
53 private $httpClient;
54
55 /**
56 * @var CacheInterface
57 */
58 private $cache;
59
60 /**
61 * @var string Decrypted Gemini API key, possibly empty.
62 */
63 private $apiKey;
64
65 /**
66 * @var array<string,string>|null In-request memoization.
67 */
68 private $memoizedModels = null;
69
70 public function __construct(
71 HttpClientInterface $httpClient,
72 CacheInterface $cache,
73 string $apiKey
74 ) {
75 $this->httpClient = $httpClient;
76 $this->cache = $cache;
77 $this->apiKey = $apiKey;
78 }
79
80 public function hasApiKey(): bool
81 {
82 return $this->apiKey !== '';
83 }
84
85 /**
86 * Return the available models as `id => display_name`, ordered with the
87 * most recent first. Falls back to the static list on any failure.
88 *
89 * @return array<string,string>
90 */
91 public function getAvailableModels(): array
92 {
93 if ($this->memoizedModels !== null) {
94 return $this->memoizedModels;
95 }
96
97 $cached = $this->cache->get(Constants::AATXT_GEMINI_MODELS_CACHE_KEY);
98 if (is_array($cached)) {
99 return $this->memoizedModels = $cached;
100 }
101
102 if (!$this->hasApiKey()) {
103 return $this->memoizedModels = $this->fallbackList();
104 }
105
106 try {
107 $models = $this->fetchFromApi();
108 } catch (Exception $e) {
109 $fallback = $this->fallbackList();
110 $this->cache->set(
111 Constants::AATXT_GEMINI_MODELS_CACHE_KEY,
112 $fallback,
113 self::CACHE_TTL_FALLBACK
114 );
115 return $this->memoizedModels = $fallback;
116 }
117
118 if (empty($models)) {
119 return $this->memoizedModels = $this->fallbackList();
120 }
121
122 $this->cache->set(
123 Constants::AATXT_GEMINI_MODELS_CACHE_KEY,
124 $models,
125 self::CACHE_TTL_SUCCESS
126 );
127 return $this->memoizedModels = $models;
128 }
129
130 public function isAvailable(string $modelId): bool
131 {
132 if ($modelId === '') {
133 return false;
134 }
135 return array_key_exists($modelId, $this->getAvailableModels());
136 }
137
138 /**
139 * Return the model id used as runtime fallback when the configured model is
140 * no longer available, favouring the least expensive one.
141 *
142 * Google does not expose pricing through the API, so the model family is
143 * used as a proxy: "flash-lite" variants are cheaper than "flash" variants,
144 * which are cheaper than the "pro" models. Within the same family the most
145 * recent model wins (the list is sorted newest-first). When no
146 * "flash-lite"/"flash" tier is available the most recent model is returned
147 * as a last resort.
148 */
149 public function getDefaultModel(): string
150 {
151 $models = $this->getAvailableModels();
152 if (empty($models)) {
153 return Constants::AATXT_GEMINI_FALLBACK_MODEL;
154 }
155
156 foreach (['flash-lite', 'flash'] as $family) {
157 foreach (array_keys($models) as $modelId) {
158 if (strpos($modelId, '-' . $family) !== false) {
159 return (string) $modelId;
160 }
161 }
162 }
163
164 // No "flash" tier is available: as a last resort return the most
165 // recent model (the list is sorted newest-first).
166 return (string) array_key_first($models);
167 }
168
169 public function flushCache(): void
170 {
171 $this->cache->delete(Constants::AATXT_GEMINI_MODELS_CACHE_KEY);
172 $this->memoizedModels = null;
173 }
174
175 /**
176 * @return array<string,string>
177 * @throws Exception
178 */
179 private function fetchFromApi(): array
180 {
181 $headers = [
182 'x-goog-api-key' => $this->apiKey,
183 ];
184
185 // The endpoint is paginated (default page size 50): request a large
186 // page to get the whole catalog in a single call.
187 $data = $this->httpClient->get(Constants::AATXT_GEMINI_MODELS_ENDPOINT . '?pageSize=1000', $headers);
188 $items = isset($data['models']) && is_array($data['models']) ? $data['models'] : [];
189
190 $items = array_values(array_filter($items, function ($item) {
191 if (!is_array($item)) {
192 return false;
193 }
194 $id = $this->modelId($item);
195 if ($id === '') {
196 return false;
197 }
198 if (!preg_match(self::INCLUDED_FAMILIES_PATTERN, $id)
199 || preg_match(self::EXCLUDED_VARIANTS_PATTERN, $id)) {
200 return false;
201 }
202 // When the API declares the supported generation methods, keep only
203 // the models able to generate content from a prompt.
204 if (isset($item['supportedGenerationMethods']) && is_array($item['supportedGenerationMethods'])) {
205 return in_array('generateContent', $item['supportedGenerationMethods'], true);
206 }
207 return true;
208 }));
209
210 // The endpoint does not expose a creation date: sort by the version
211 // number embedded in the id (e.g. "gemini-3.5-flash" => 3.5) so the
212 // most recent family comes first.
213 usort($items, function ($a, $b) {
214 return $this->modelVersion($this->modelId($b)) <=> $this->modelVersion($this->modelId($a));
215 });
216
217 $result = [];
218 foreach ($items as $item) {
219 $id = $this->modelId($item);
220 $display = isset($item['displayName']) && $item['displayName'] !== ''
221 ? (string) $item['displayName']
222 : $id;
223 $result[$id] = $display;
224 }
225
226 return $result;
227 }
228
229 /**
230 * Extract the bare model id from an API item, stripping the
231 * "models/" resource prefix (e.g. "models/gemini-3.5-flash").
232 *
233 * @param array<string, mixed> $item
234 */
235 private function modelId(array $item): string
236 {
237 $name = isset($item['name']) ? (string) $item['name'] : '';
238 if ($name === '') {
239 return '';
240 }
241 return strpos($name, 'models/') === 0 ? substr($name, strlen('models/')) : $name;
242 }
243
244 /**
245 * Extract the family version number from a model id
246 * (e.g. "gemini-3.5-flash" => 3.5). Unknown formats sort last.
247 */
248 private function modelVersion(string $modelId): float
249 {
250 if (preg_match('/^gemini-(\d+(?:\.\d+)?)/i', $modelId, $matches)) {
251 return (float) $matches[1];
252 }
253 return 0.0;
254 }
255
256 /**
257 * @return array<string,string>
258 */
259 private function fallbackList(): array
260 {
261 return Constants::AATXT_OPTION_FIELD_MODEL_GEMINI_OPTIONS;
262 }
263 }
264