diff --git a/README.md b/README.md
index 2676696..87b722d 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,6 @@ Szkielet komponentu Joomla 6 do gromadzenia pytań testowych.
## Instalacja
-W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.43.26).
+W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.43.27).
Po instalacji komponent jest dostępny w panelu administratora jako **Pytania testowe**.
diff --git a/administrator/components/com_testquestions/config.xml b/administrator/components/com_testquestions/config.xml
index cb3f55c..c66344b 100644
--- a/administrator/components/com_testquestions/config.xml
+++ b/administrator/components/com_testquestions/config.xml
@@ -5,6 +5,7 @@
+
diff --git a/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini b/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini
index b6bcfbb..29daffa 100644
--- a/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini
+++ b/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini
@@ -71,8 +71,10 @@ COM_TESTQUESTIONS_AI_PROVIDER="Dostawca AI"
COM_TESTQUESTIONS_AI_API_KEY="Klucz API"
COM_TESTQUESTIONS_AI_OPENAI_KEY="Klucz API OpenAI"
COM_TESTQUESTIONS_AI_GROK_KEY="Klucz API Grok"
+COM_TESTQUESTIONS_AI_GROQ_KEY="Klucz API Groq"
COM_TESTQUESTIONS_AI_OPENAI_MODEL="Model OpenAI"
COM_TESTQUESTIONS_AI_GROK_MODEL="Model Grok"
+COM_TESTQUESTIONS_AI_GROQ_MODEL="Model Groq API"
COM_TESTQUESTIONS_AI_MODEL_INVALID="Wybierz prawidłowy model dla każdego dostawcy AI."
COM_TESTQUESTIONS_AI_TOKEN_USAGE="Wykorzystanie tokenów API"
COM_TESTQUESTIONS_AI_TOKEN_USAGE_INFO="Suma użycia zapisanego przy ocenach AI we wszystkich wynikach testów. Powtórne oceny są zliczane, jeśli zostały wykonane po tej aktualizacji."
diff --git a/administrator/components/com_testquestions/sql/updates/mysql/1.43.27.sql b/administrator/components/com_testquestions/sql/updates/mysql/1.43.27.sql
new file mode 100644
index 0000000..30a8399
--- /dev/null
+++ b/administrator/components/com_testquestions/sql/updates/mysql/1.43.27.sql
@@ -0,0 +1 @@
+-- Wydanie 1.43.27: dodano dostawcę Groq API.
diff --git a/administrator/components/com_testquestions/src/Controller/AiController.php b/administrator/components/com_testquestions/src/Controller/AiController.php
index ab4dea5..54bd177 100644
--- a/administrator/components/com_testquestions/src/Controller/AiController.php
+++ b/administrator/components/com_testquestions/src/Controller/AiController.php
@@ -36,14 +36,14 @@ class AiController extends BaseController
$input = Factory::getApplication()->getInput();
$provider = strtolower($input->post->getCmd('ai_provider', ''));
- if (!in_array($provider, ['openai', 'grok'], true)) {
+ if (!in_array($provider, ['openai', 'grok', 'groq'], true)) {
throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_PROVIDER_INVALID'));
}
$models = AiService::availableModels();
$selectedModels = [];
- foreach (['openai', 'grok'] as $modelProvider) {
- $model = $input->post->getCmd('ai_' . $modelProvider . '_model', '');
+ foreach (['openai', 'grok', 'groq'] 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'] as $keyProvider) {
+ foreach (['openai', 'grok', 'groq'] as $keyProvider) {
$fieldName = 'ai_' . $keyProvider . '_api_key';
$apiKey = trim((string) $input->post->get($fieldName, '', 'RAW'));
if ($apiKey !== '') {
diff --git a/administrator/components/com_testquestions/src/Model/ResultModel.php b/administrator/components/com_testquestions/src/Model/ResultModel.php
index 777c2df..56a4e92 100644
--- a/administrator/components/com_testquestions/src/Model/ResultModel.php
+++ b/administrator/components/com_testquestions/src/Model/ResultModel.php
@@ -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'];
+ $defaultModels = ['openai' => 'gpt-4.1-mini', 'grok' => 'grok-4.7', 'groq' => 'openai/gpt-oss-120b'];
$model = trim((string) $params->get('ai_' . $provider . '_model', $defaultModels[$provider] ?? ''));
// Odczytaj zapisane parametry bezpośrednio z bazy. Dzięki temu nie użyjemy
diff --git a/administrator/components/com_testquestions/src/Model/ResultsModel.php b/administrator/components/com_testquestions/src/Model/ResultsModel.php
index 819ca43..d1c4358 100644
--- a/administrator/components/com_testquestions/src/Model/ResultsModel.php
+++ b/administrator/components/com_testquestions/src/Model/ResultsModel.php
@@ -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];
+ $item->ai_tokens_by_provider = ['openai' => 0, 'grok' => 0, 'groq' => 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],
+ 'providers' => ['openai' => $emptyProviderTotals, 'grok' => $emptyProviderTotals, 'groq' => $emptyProviderTotals],
];
foreach ($answersRows as $answersJson) {
diff --git a/administrator/components/com_testquestions/src/Service/AiService.php b/administrator/components/com_testquestions/src/Service/AiService.php
index ed30dc4..17fae29 100644
--- a/administrator/components/com_testquestions/src/Service/AiService.php
+++ b/administrator/components/com_testquestions/src/Service/AiService.php
@@ -23,6 +23,11 @@ class AiService
'grok-4.20' => 'Grok 4.20 Reasoning',
'grok-4.20-non-reasoning' => 'Grok 4.20 Non-Reasoning',
],
+ 'groq' => [
+ 'openai/gpt-oss-120b' => 'GPT OSS 120B',
+ 'openai/gpt-oss-20b' => 'GPT OSS 20B',
+ 'llama-3.3-70b-versatile' => 'Llama 3.3 70B Versatile',
+ ],
];
}
@@ -30,15 +35,17 @@ class AiService
{
$provider = strtolower(trim($provider));
$provider = preg_replace('/[^a-z]/', '', $provider);
- $config = $provider === 'grok'
- ? ['url' => 'https://api.x.ai/v1/responses']
- : ['url' => 'https://api.openai.com/v1/responses'];
+ $config = match ($provider) {
+ 'grok' => ['url' => 'https://api.x.ai/v1/responses'],
+ 'groq' => ['url' => 'https://api.groq.com/openai/v1/chat/completions'],
+ default => ['url' => 'https://api.openai.com/v1/responses'],
+ };
if (trim($apiKey) === '') {
throw new \RuntimeException('AI_NOT_CONFIGURED');
}
- if (!in_array($provider, ['openai', 'grok'], true)) {
+ if (!in_array($provider, ['openai', 'grok', 'groq'], true)) {
throw new \RuntimeException('Nieprawidłowy dostawca AI: ' . $provider);
}
@@ -54,12 +61,18 @@ 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 = [
- 'model' => $model,
- 'input' => $input,
- ];
+ $requestData = $provider === 'groq'
+ ? [
+ 'model' => $model,
+ 'messages' => [['role' => 'user', 'content' => $input]],
+ 'temperature' => 0.1,
+ ]
+ : [
+ 'model' => $model,
+ 'input' => $input,
+ ];
// GPT-5.x reasoning models do not accept a custom temperature.
- if ($provider === 'grok' || $model === 'gpt-4.1-mini') {
+ if ($provider === 'grok' || ($provider === 'openai' && $model === 'gpt-4.1-mini')) {
$requestData['temperature'] = 0.1;
}
$payload = json_encode($requestData, JSON_UNESCAPED_UNICODE);
@@ -115,12 +128,16 @@ class AiService
private function getUsage(array $body, string $provider, string $model): ?array
{
$usage = $body['usage'] ?? null;
- if (!is_array($usage) || !isset($usage['input_tokens'], $usage['output_tokens'])) {
+ if (!is_array($usage)) {
return null;
}
- $inputTokens = max(0, (int) $usage['input_tokens']);
- $outputTokens = max(0, (int) $usage['output_tokens']);
+ $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']))) {
+ return null;
+ }
$cachedTokens = min($inputTokens, max(0, (int) ($usage['input_tokens_details']['cached_tokens'] ?? 0)));
$cost = null;
$costSource = '';
@@ -162,6 +179,10 @@ class AiService
private function extractText(array $body): string
{
+ if (isset($body['choices'][0]['message']['content']) && is_string($body['choices'][0]['message']['content'])) {
+ return trim($body['choices'][0]['message']['content']);
+ }
+
if (!empty($body['output_text']) && is_string($body['output_text'])) {
return trim($body['output_text']);
}
diff --git a/administrator/components/com_testquestions/src/View/Ai/HtmlView.php b/administrator/components/com_testquestions/src/View/Ai/HtmlView.php
index 4aa02e2..17320f7 100644
--- a/administrator/components/com_testquestions/src/View/Ai/HtmlView.php
+++ b/administrator/components/com_testquestions/src/View/Ai/HtmlView.php
@@ -28,6 +28,7 @@ class HtmlView extends BaseHtmlView
$this->usageTotals = [
'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],
];
$usageRows = $db->setQuery(
diff --git a/administrator/components/com_testquestions/tmpl/ai/default.php b/administrator/components/com_testquestions/tmpl/ai/default.php
index f977991..187ef1d 100644
--- a/administrator/components/com_testquestions/tmpl/ai/default.php
+++ b/administrator/components/com_testquestions/tmpl/ai/default.php
@@ -7,8 +7,10 @@
settings['ai_api_key'] ?? ''); ?>
settings['ai_openai_api_key'] ?? ($provider === 'openai' ? $legacyKey : '')); ?>
settings['ai_grok_api_key'] ?? ($provider === 'grok' ? $legacyKey : '')); ?>
+settings['ai_groq_api_key'] ?? ($provider === 'groq' ? $legacyKey : '')); ?>
settings['ai_openai_model'] ?? 'gpt-4.1-mini'); ?>
settings['ai_grok_model'] ?? 'grok-4.7'); ?>
+settings['ai_groq_model'] ?? 'openai/gpt-oss-120b'); ?>