Show actual AI token usage and cost

This commit is contained in:
Wojtek
2026-10-16 13:08:59 +02:00
parent 6daca5be59
commit 7d9efd64f4
6 changed files with 73 additions and 2 deletions
@@ -0,0 +1 @@
-- No database changes required for version 1.43.4.
@@ -151,6 +151,9 @@ class ResultModel extends BaseDatabaseModel
);
$item->answers[$questionId]['ai_feedback'] = $aiResult['feedback'];
$item->answers[$questionId]['awarded_points'] = $aiResult['awarded_points'];
if ($aiResult['usage'] !== null) {
$item->answers[$questionId]['ai_usage'] = $aiResult['usage'];
}
$correctionPoints = 0;
foreach ($item->answers as $savedAnswer) {
@@ -73,9 +73,46 @@ class AiService
throw new \RuntimeException('AI zwróciło nieprawidłowy format oceny i punktów.');
}
$usage = $this->getUsage($body, $provider, $config['model']);
return [
'awarded_points' => min(max(0, $maxPoints), max(0, (int) $result['awarded_points'])),
'feedback' => trim((string) ($result['feedback'] ?? '')),
'usage' => $usage,
];
}
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'])) {
return null;
}
$inputTokens = max(0, (int) $usage['input_tokens']);
$outputTokens = max(0, (int) $usage['output_tokens']);
$cachedTokens = min($inputTokens, max(0, (int) ($usage['input_tokens_details']['cached_tokens'] ?? 0)));
$cost = null;
$costSource = '';
if ($provider === 'grok' && isset($usage['cost_in_usd_ticks']) && is_numeric($usage['cost_in_usd_ticks'])) {
$cost = (float) $usage['cost_in_usd_ticks'] / 10000000000;
$costSource = 'provider';
} elseif ($provider === 'openai' && $model === 'gpt-4.1-mini') {
$uncachedTokens = $inputTokens - $cachedTokens;
$cost = ($uncachedTokens * 0.40 + $cachedTokens * 0.10 + $outputTokens * 1.60) / 1000000;
$costSource = 'token_rates';
}
return [
'provider' => $provider,
'model' => $model,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'total_tokens' => (int) ($usage['total_tokens'] ?? ($inputTokens + $outputTokens)),
'cached_input_tokens' => $cachedTokens,
'cost_usd' => $cost,
'cost_source' => $costSource,
];
}
@@ -35,6 +35,25 @@
<button type="submit" class="btn btn-primary" formaction="<?php echo Route::_('index.php?option=com_testquestions&task=result.aiAll&id=' . (int) $this->item->id); ?>" onclick="this.form.task.value='result.aiAll';">
<?php echo \Joomla\CMS\Language\Text::sprintf('COM_TESTQUESTIONS_AI_EVALUATE_ALL', $pendingAiCount); ?>
</button>
<?php
$usageTotals = ['input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'providers' => []];
foreach ($this->item->answers as $savedAnswer) {
$usage = $savedAnswer['ai_usage'] ?? null;
if (!is_array($usage)) continue;
$usageTotals['input_tokens'] += (int) ($usage['input_tokens'] ?? 0);
$usageTotals['output_tokens'] += (int) ($usage['output_tokens'] ?? 0);
$usageTotals['total_tokens'] += (int) ($usage['total_tokens'] ?? 0);
if (is_numeric($usage['cost_usd'] ?? null)) $usageTotals['cost_usd'] += (float) $usage['cost_usd'];
if (!empty($usage['provider'])) $usageTotals['providers'][(string) $usage['provider']] = true;
}
?>
<?php if ($usageTotals['total_tokens'] > 0) : ?>
<span class="badge text-bg-light border ms-2" title="Suma dotychczasowych ocen AI">
✦ <?php echo implode(' + ', array_map('htmlspecialchars', array_keys($usageTotals['providers']))); ?>:
<?php echo number_format($usageTotals['total_tokens'], 0, ',', ' '); ?> tokenów,
<?php echo number_format($usageTotals['cost_usd'], 8, ',', ' '); ?> USD
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<h2><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_RESULT_ANSWERS'); ?></h2>
@@ -55,6 +74,17 @@
<button type="submit" name="ai_question_id" value="<?php echo (int) $questionId; ?>" onclick="this.form.task.value='result.ai';" formaction="<?php echo Route::_('index.php?option=com_testquestions&task=result.ai&id=' . (int) $this->item->id); ?>" class="btn btn-outline-primary">
<?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_EVALUATE'); ?>
</button>
<?php if (!empty($answer['ai_usage']) && is_array($answer['ai_usage'])) : ?>
<?php $usage = $answer['ai_usage']; ?>
<?php $usageProvider = (string) ($usage['provider'] ?? ''); ?>
<span class="badge text-bg-light border ms-2" title="<?php echo $usageProvider === 'grok' ? 'Koszt zwrócony przez dostawcę' : 'Koszt obliczony z faktycznie wykorzystanych tokenów i stawek modelu'; ?>">
<?php echo $usageProvider === 'grok' ? '✦ Grok' : '◉ OpenAI'; ?> ·
<?php echo number_format((int) ($usage['input_tokens'] ?? 0), 0, ',', ' '); ?> in /
<?php echo number_format((int) ($usage['output_tokens'] ?? 0), 0, ',', ' '); ?> out ·
<?php echo is_numeric($usage['cost_usd'] ?? null) ? number_format((float) $usage['cost_usd'], 8, ',', ' ') . ' USD' : 'koszt niedostępny'; ?>
<?php echo ($usage['cost_source'] ?? '') === 'token_rates' ? ' (wyliczony)' : ''; ?>
</span>
<?php endif; ?>
</div>
<?php if (!empty($answer['ai_feedback'])) : ?>
<div class="alert alert-info mt-3"><strong><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_RESULT'); ?>:</strong><br><?php echo nl2br(htmlspecialchars((string) $answer['ai_feedback'], ENT_QUOTES, 'UTF-8')); ?></div>