Add Gemini API as an AI provider
This commit is contained in:
@@ -36,13 +36,13 @@ class AiController extends BaseController
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$provider = strtolower($input->post->getCmd('ai_provider', ''));
|
||||
if (!in_array($provider, ['openai', 'grok', 'groq'], true)) {
|
||||
if (!in_array($provider, ['openai', 'grok', 'groq', 'gemini'], true)) {
|
||||
throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_PROVIDER_INVALID'));
|
||||
}
|
||||
|
||||
$models = AiService::availableModels();
|
||||
$selectedModels = [];
|
||||
foreach (['openai', 'grok', 'groq'] as $modelProvider) {
|
||||
foreach (['openai', 'grok', 'groq', 'gemini'] as $modelProvider) {
|
||||
$model = trim($input->post->getString('ai_' . $modelProvider . '_model', ''));
|
||||
if (!isset($models[$modelProvider][$model])) {
|
||||
throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_MODEL_INVALID'));
|
||||
@@ -71,7 +71,7 @@ class AiController extends BaseController
|
||||
$settings['ai_' . $modelProvider . '_model'] = $model;
|
||||
}
|
||||
|
||||
foreach (['openai', 'grok', 'groq'] as $keyProvider) {
|
||||
foreach (['openai', 'grok', 'groq', 'gemini'] as $keyProvider) {
|
||||
$fieldName = 'ai_' . $keyProvider . '_api_key';
|
||||
$apiKey = trim((string) $input->post->get($fieldName, '', 'RAW'));
|
||||
if ($apiKey !== '') {
|
||||
|
||||
@@ -96,7 +96,7 @@ class ResultModel extends BaseDatabaseModel
|
||||
$keyName = 'ai_' . $provider . '_api_key';
|
||||
$apiKey = trim((string) $params->get($keyName, $params->get('ai_api_key', '')));
|
||||
$componentKeyLength = mb_strlen($apiKey);
|
||||
$defaultModels = ['openai' => 'gpt-4.1-mini', 'grok' => 'grok-4.7', 'groq' => 'openai/gpt-oss-120b'];
|
||||
$defaultModels = ['openai' => 'gpt-4.1-mini', 'grok' => 'grok-4.7', 'groq' => 'openai/gpt-oss-120b', 'gemini' => 'gemini-3.8-flash'];
|
||||
$model = trim((string) $params->get('ai_' . $provider . '_model', $defaultModels[$provider] ?? ''));
|
||||
|
||||
// Odczytaj zapisane parametry bezpośrednio z bazy. Dzięki temu nie użyjemy
|
||||
|
||||
@@ -18,7 +18,7 @@ class ResultsModel extends ListModel
|
||||
$item->ai_output_tokens = 0;
|
||||
$item->ai_total_tokens = 0;
|
||||
$item->ai_cost_usd = 0.0;
|
||||
$item->ai_tokens_by_provider = ['openai' => 0, 'grok' => 0, 'groq' => 0];
|
||||
$item->ai_tokens_by_provider = ['openai' => 0, 'grok' => 0, 'groq' => 0, 'gemini' => 0];
|
||||
|
||||
$answers = json_decode((string) ($item->answers ?? ''), true);
|
||||
if (!is_array($answers)) {
|
||||
@@ -68,7 +68,7 @@ class ResultsModel extends ListModel
|
||||
'output_tokens' => 0,
|
||||
'total_tokens' => 0,
|
||||
'cost_usd' => 0.0,
|
||||
'providers' => ['openai' => $emptyProviderTotals, 'grok' => $emptyProviderTotals, 'groq' => $emptyProviderTotals],
|
||||
'providers' => ['openai' => $emptyProviderTotals, 'grok' => $emptyProviderTotals, 'groq' => $emptyProviderTotals, 'gemini' => $emptyProviderTotals],
|
||||
];
|
||||
|
||||
foreach ($answersRows as $answersJson) {
|
||||
|
||||
@@ -28,6 +28,10 @@ class AiService
|
||||
'openai/gpt-oss-20b' => 'GPT OSS 20B',
|
||||
'llama-3.3-70b-versatile' => 'Llama 3.3 70B Versatile',
|
||||
],
|
||||
'gemini' => [
|
||||
'gemini-3.8-flash' => 'Gemini 3.8 Flash',
|
||||
'gemini-3.1-pro-preview' => 'Gemini 3.1 Pro (Preview)',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -38,6 +42,7 @@ class AiService
|
||||
$config = match ($provider) {
|
||||
'grok' => ['url' => 'https://api.x.ai/v1/responses'],
|
||||
'groq' => ['url' => 'https://api.groq.com/openai/v1/chat/completions'],
|
||||
'gemini' => ['url' => 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($model) . ':generateContent'],
|
||||
default => ['url' => 'https://api.openai.com/v1/responses'],
|
||||
};
|
||||
|
||||
@@ -45,7 +50,7 @@ class AiService
|
||||
throw new \RuntimeException('AI_NOT_CONFIGURED');
|
||||
}
|
||||
|
||||
if (!in_array($provider, ['openai', 'grok', 'groq'], true)) {
|
||||
if (!in_array($provider, ['openai', 'grok', 'groq', 'gemini'], true)) {
|
||||
throw new \RuntimeException('Nieprawidłowy dostawca AI: ' . $provider);
|
||||
}
|
||||
|
||||
@@ -61,29 +66,40 @@ class AiService
|
||||
. "Odpowiedź ucznia:\n" . ($answer !== '' ? $answer : 'Brak odpowiedzi') . "\n\n"
|
||||
. "Zwróć wyłącznie poprawny obiekt JSON w formacie {\"awarded_points\": liczba_całkowita, \"feedback\": \"krótka ocena po polsku\"}. W ocenie opisz poprawne i brakujące elementy. Nie zmieniaj wzorca odpowiedzi.";
|
||||
|
||||
$requestData = $provider === 'groq'
|
||||
? [
|
||||
$requestData = match ($provider) {
|
||||
'groq' => [
|
||||
'model' => $model,
|
||||
'messages' => [['role' => 'user', 'content' => $input]],
|
||||
'temperature' => 0.1,
|
||||
]
|
||||
: [
|
||||
],
|
||||
'gemini' => [
|
||||
'contents' => [['parts' => [['text' => $input]]]],
|
||||
'generationConfig' => ['responseMimeType' => 'application/json'],
|
||||
],
|
||||
default => [
|
||||
'model' => $model,
|
||||
'input' => $input,
|
||||
];
|
||||
],
|
||||
};
|
||||
// GPT-5.x reasoning models do not accept a custom temperature.
|
||||
if ($provider === 'grok' || ($provider === 'openai' && $model === 'gpt-4.1-mini')) {
|
||||
$requestData['temperature'] = 0.1;
|
||||
}
|
||||
$payload = json_encode($requestData, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
if ($provider === 'gemini') {
|
||||
$headers['x-goog-api-key'] = trim($apiKey);
|
||||
} else {
|
||||
$headers['Authorization'] = 'Bearer ' . trim($apiKey);
|
||||
}
|
||||
|
||||
$response = HttpFactory::getHttp()->post(
|
||||
$config['url'],
|
||||
$payload,
|
||||
[
|
||||
'Authorization' => 'Bearer ' . trim($apiKey),
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
$headers,
|
||||
60
|
||||
);
|
||||
$body = json_decode((string) $response->body, true);
|
||||
@@ -132,13 +148,15 @@ class AiService
|
||||
return null;
|
||||
}
|
||||
|
||||
$inputTokens = max(0, (int) ($usage['input_tokens'] ?? $usage['prompt_tokens'] ?? 0));
|
||||
$outputTokens = max(0, (int) ($usage['output_tokens'] ?? $usage['completion_tokens'] ?? 0));
|
||||
if ((!isset($usage['input_tokens']) && !isset($usage['prompt_tokens']))
|
||||
|| (!isset($usage['output_tokens']) && !isset($usage['completion_tokens']))) {
|
||||
$inputTokens = max(0, (int) ($usage['input_tokens'] ?? $usage['prompt_tokens'] ?? $usage['promptTokenCount'] ?? 0));
|
||||
$outputTokens = max(0, (int) ($usage['output_tokens'] ?? $usage['completion_tokens'] ?? (
|
||||
(int) ($usage['candidatesTokenCount'] ?? 0) + (int) ($usage['thoughtsTokenCount'] ?? 0)
|
||||
)));
|
||||
if ((!isset($usage['input_tokens']) && !isset($usage['prompt_tokens']) && !isset($usage['promptTokenCount']))
|
||||
|| (!isset($usage['output_tokens']) && !isset($usage['completion_tokens']) && !isset($usage['candidatesTokenCount']))) {
|
||||
return null;
|
||||
}
|
||||
$cachedTokens = min($inputTokens, max(0, (int) ($usage['input_tokens_details']['cached_tokens'] ?? 0)));
|
||||
$cachedTokens = min($inputTokens, max(0, (int) ($usage['input_tokens_details']['cached_tokens'] ?? $usage['cachedContentTokenCount'] ?? 0)));
|
||||
$cost = null;
|
||||
$costSource = '';
|
||||
|
||||
@@ -159,7 +177,7 @@ class AiService
|
||||
'model' => $model,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens' => $outputTokens,
|
||||
'total_tokens' => (int) ($usage['total_tokens'] ?? ($inputTokens + $outputTokens)),
|
||||
'total_tokens' => (int) ($usage['total_tokens'] ?? $usage['totalTokenCount'] ?? ($inputTokens + $outputTokens)),
|
||||
'cached_input_tokens' => $cachedTokens,
|
||||
'cost_usd' => $cost,
|
||||
'cost_source' => $costSource,
|
||||
@@ -179,6 +197,16 @@ class AiService
|
||||
|
||||
private function extractText(array $body): string
|
||||
{
|
||||
if (isset($body['candidates'][0]['content']['parts']) && is_array($body['candidates'][0]['content']['parts'])) {
|
||||
$parts = [];
|
||||
foreach ($body['candidates'][0]['content']['parts'] as $part) {
|
||||
if (isset($part['text']) && is_string($part['text'])) {
|
||||
$parts[] = $part['text'];
|
||||
}
|
||||
}
|
||||
return trim(implode("\n", $parts));
|
||||
}
|
||||
|
||||
if (isset($body['choices'][0]['message']['content']) && is_string($body['choices'][0]['message']['content'])) {
|
||||
return trim($body['choices'][0]['message']['content']);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class HtmlView extends BaseHtmlView
|
||||
'openai' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
|
||||
'grok' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
|
||||
'groq' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
|
||||
'gemini' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
|
||||
];
|
||||
|
||||
$usageRows = $db->setQuery(
|
||||
|
||||
Reference in New Issue
Block a user