Skip to content

Instantly share code, notes, and snippets.

@Megafry
Created June 1, 2026 19:55
Show Gist options
  • Select an option

  • Save Megafry/6a69495fd45486669f632c66ce1d6771 to your computer and use it in GitHub Desktop.

Select an option

Save Megafry/6a69495fd45486669f632c66ce1d6771 to your computer and use it in GitHub Desktop.
Test: Registering a custom API provider for digitalpulsebe craft-multi-translator
{% import '_includes/forms.twig' as forms %}
{{ forms.autosuggestField({
label: 'Gemini API Key',
name: 'apiKey',
suggestEnvVars: true,
value: settings.apiKey ?? '',
}) }}
{{ forms.selectField({
label: 'Model',
name: 'model',
value: settings.model ?? 'gemini-2.5-flash',
options: modelOptions,
}) }}
<?php
declare(strict_types=1);
namespace modules\translator\providers;
use Craft;
use craft\helpers\App;
use digitalpulsebe\craftmultitranslator\providers\Provider;
use digitalpulsebe\craftmultitranslator\MultiTranslator;
use GeminiAPI\Client;
use GeminiAPI\GenerationConfig;
use GeminiAPI\Resources\Parts\TextPart;
use Throwable;
class GeminiProvider extends Provider
{
private const DEFAULT_MODEL = 'gemini-3.1-flash-lite';
private const MODEL_CACHE_TTL = 3600;
private const MODELS_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
private ?Client $client = null;
// -------------------------------------------------------------------------
// Identity
// -------------------------------------------------------------------------
public static function getHandle(): string
{
return 'gemini';
}
public static function getDisplayName(): string
{
return 'Google Gemini';
}
// -------------------------------------------------------------------------
// Settings / templates
// -------------------------------------------------------------------------
public function getSettingsTemplatePath(): ?string
{
return 'translator/gemini/_settings';
}
public function getSettingsTemplateVariables(): array
{
return [
'settings' => $this->settings,
'modelOptions' => $this->getAvailableModels()
];
}
// -------------------------------------------------------------------------
// Connectivity
// -------------------------------------------------------------------------
public function isConnected(): bool
{
return !empty($this->getApiKey());
}
private function getApiKey(): string
{
return App::parseEnv($this->getSetting('apiKey', ''));
}
/**
* Lazily creates and reuses a single Client instance per request.
*
* @throws \RuntimeException if no API key is configured
*/
private function getClient(): Client
{
if ($this->client !== null) {
return $this->client;
}
$apiKey = $this->getApiKey();
if (empty($apiKey)) {
throw new \RuntimeException('Gemini API key is not configured.');
}
return $this->client = new Client($apiKey);
}
// -------------------------------------------------------------------------
// Model discovery
// -------------------------------------------------------------------------
public function getAvailableModels(): array
{
$apiKey = $this->getApiKey();
if (empty($apiKey)) {
return [];
}
$cacheKey = 'gemini_available_models_' . md5($apiKey);
return Craft::$app->getCache()->getOrSet(
$cacheKey,
fn() => $this->fetchAvailableModels($apiKey),
self::MODEL_CACHE_TTL
);
}
/**
* Fetches and normalises the list of content-generation-capable models.
*/
private function fetchAvailableModels(string $apiKey): array
{
try {
$response = Craft::createGuzzleClient()->get(
self::MODELS_ENDPOINT,
['query' => ['key' => $apiKey]]
);
$data = json_decode(
(string) $response->getBody(),
true,
512,
JSON_THROW_ON_ERROR
);
return $this->normaliseModelList($data['models'] ?? []);
} catch (Throwable $e) {
Craft::error(
sprintf('Failed to fetch Gemini models: %s', $e->getMessage()),
__METHOD__
);
return [];
}
}
/**
* Filters, shapes, and sorts raw model records from the API.
*
* @param array<int, array<string, mixed>> $models
* @return array<int, array{label: string, value: string}>
*/
private function normaliseModelList(array $models): array
{
$options = [];
foreach ($models as $model) {
if (!in_array('generateContent', $model['supportedGenerationMethods'] ?? [], true)) {
continue;
}
$name = str_replace('models/', '', $model['name'] ?? '');
$options[] = [
'label' => $model['displayName'] ?? $name,
'value' => $name,
];
}
usort($options, fn(array $a, array $b): int => strcmp($a['label'], $b['label']));
return $options;
}
// -------------------------------------------------------------------------
// Translation
// -------------------------------------------------------------------------
public function translate(
string $sourceLocale = null,
string $targetLocale = null,
string $text = null
): ?string {
if (empty($text) || empty($sourceLocale) || empty($targetLocale)) {
return null;
}
try {
$response = $this->getClient()
->generativeModel($this->getModel())
->generateContent(
new TextPart('You are a professional translator. Return only the translated text, no explanations, notes, quotes, or markdown.'),
new TextPart("Translate from {$sourceLocale} to {$targetLocale}:"),
new TextPart($text)
);
$result = trim($response->text());
return $result !== '' ? $result : null;
} catch (\RuntimeException $e) {
// Configuration error — surface clearly
MultiTranslator::error(
sprintf('Gemini configuration error: %s', $e->getMessage()),
);
throw $e;
} catch (Throwable $e) {
MultiTranslator::error(
sprintf(
'Gemini translation failed [%s → %s]: %s',
$sourceLocale,
$targetLocale,
$e->getMessage()
)
);
throw $e;
}
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
private function getModel(): string
{
return (string) ($this->getSetting('model') ?: self::DEFAULT_MODEL);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment