feat: costo OCR en dashboard, logs correctos y fix de thinking tokens

- OcrExtractionService: registra cada llamada en ai_usage_logs (servicio=ocr,
  costo=5 COP/foto) y ahora tambien loggea cuando SI funciona, no solo
  cuando falla (antes era imposible confirmar por log que el OCR corrio).
- GeminiVisionService: el log decia siempre "[Gemini Vision]" sin importar
  si la llamada fue de imagen o de texto post-OCR, haciendo imposible
  distinguir cual camino se uso realmente. Ahora usa el servicio real.
- GeminiVisionService: agrega thinkingConfig.thinkingBudget=0 (igual que
  GeminiIntentService) para que el modelo no gaste tokens narrando su
  razonamiento en texto plano antes del JSON final -- se estaba colando
  ese razonamiento en la respuesta y comiendose el presupuesto de tokens.
- GeminiIntentService: resultado ya no queda forzado a 'ok'; ahora filtra
  bloques 'thought' igual que vision, y guarda tokens/raw en el detalle
  para poder diagnosticar sin acceso al servidor.
- ShowAiLogs: card y filtro de costo OCR en el dashboard de consumo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-08-11 01:52:59 +00:00
co-authored by Claude Sonnet 5
parent 7b21cd9773
commit 5e0c0367d9
6 changed files with 73 additions and 19 deletions
+21 -4
View File
@@ -72,8 +72,20 @@ class GeminiIntentService
$inputTokens = $response->json('usageMetadata.promptTokenCount', 0);
$outputTokens = $response->json('usageMetadata.candidatesTokenCount', 0);
$text = $response->json('candidates.0.content.parts.0.text', '');
$resultado = $this->parseRespuesta($text);
// Igual que GeminiVisionService: si el modelo manda bloques de
// "pensamiento" (thought=true), el JSON real puede no venir en la
// parte 0. Sin este filtro, parts.0.text agarraba el pensamiento
// y el parseo fallaba en silencio (quedaba marcado 'ok' igual).
$parts = $response->json('candidates.0.content.parts', []);
$text = implode("\n", array_column(
array_filter($parts, fn ($p) => isset($p['text']) && empty($p['thought'])),
'text'
));
$resultado = $this->parseRespuesta($text);
Log::info('[Gemini Intent] raw response: ' . substr($text, 0, 300));
AiUsageLog::registrar([
'servicio' => 'gemini_intent',
@@ -82,8 +94,13 @@ class GeminiIntentService
'input_tokens' => $inputTokens,
'output_tokens'=> $outputTokens,
'tiempo_ms' => $tiempoMs,
'resultado' => 'ok',
'detalle' => ['texto' => $texto, 'accion' => $resultado['action'] ?? null],
'resultado' => $resultado ? 'ok' : 'error',
'detalle' => [
'texto' => $texto,
'accion' => $resultado['action'] ?? null,
'tokens' => "{$inputTokens}/{$outputTokens}",
'raw' => $resultado ? null : substr($text, 0, 200),
],
]);
return $resultado;