fix: GeminiVisionService tries v1+v1beta, shows real API error in Telegram
- Rewrote extraerPago() to try v1 then v1beta for model compatibility - Returns __error key with actual API message when both fail - TelegramBotService shows that error in chat instead of generic message - Also ensures model is read fresh from config (no hardcoded URL in constructor) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
cfa38e51df
commit
2acb80a7d2
@@ -10,33 +10,14 @@ use Illuminate\Support\Facades\Log;
|
||||
class GeminiVisionService
|
||||
{
|
||||
private string $apiKey;
|
||||
private string $apiUrl;
|
||||
|
||||
private string $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = ChatConfig::get('gemini_api_key', '');
|
||||
$this->model = ChatConfig::get('gemini_model', 'gemini-3.5-flash');
|
||||
$ver = preg_match('/gemini-(2\.5|3[\.\d]*)/', $this->model) ? 'v1' : 'v1beta';
|
||||
$this->apiUrl = "https://generativelanguage.googleapis.com/{$ver}/models/{$this->model}:generateContent";
|
||||
}
|
||||
|
||||
private function buildRequest(array $contents, array $genConfig = []): array
|
||||
{
|
||||
return [
|
||||
'contents' => $contents,
|
||||
'generationConfig' => array_merge(['temperature' => 0.1, 'maxOutputTokens' => 200], $genConfig),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae datos de pago de una imagen de comprobante.
|
||||
*
|
||||
* @param string $base64 Imagen en base64
|
||||
* @param string $mimeType MIME type (image/jpeg, image/png, etc.)
|
||||
* @return array|null ['banco', 'valor', 'referencia', 'fecha', 'hora', 'remitente'] o null
|
||||
*/
|
||||
public function extraerPago(string $base64, string $mimeType, ?int $usuarioId = null, string $canal = 'telegram'): ?array
|
||||
{
|
||||
if (! $this->apiKey) {
|
||||
@@ -44,67 +25,74 @@ class GeminiVisionService
|
||||
return null;
|
||||
}
|
||||
|
||||
$prompt = $this->buildPrompt();
|
||||
$inicio = microtime(true);
|
||||
$contents = [[
|
||||
'parts' => [
|
||||
['text' => $this->buildPrompt()],
|
||||
['inline_data' => ['mime_type' => $mimeType, 'data' => $base64]],
|
||||
],
|
||||
]];
|
||||
|
||||
try {
|
||||
$contents = [[
|
||||
'parts' => [
|
||||
['text' => $prompt],
|
||||
['inline_data' => ['mime_type' => $mimeType, 'data' => $base64]],
|
||||
],
|
||||
]];
|
||||
$body = [
|
||||
'contents' => $contents,
|
||||
'generationConfig' => ['temperature' => 0.1, 'maxOutputTokens' => 200],
|
||||
];
|
||||
|
||||
$response = Http::withHeaders(['Content-Type' => 'application/json'])
|
||||
->timeout(15)
|
||||
->post("{$this->apiUrl}?key={$this->apiKey}", $this->buildRequest($contents));
|
||||
$inicio = microtime(true);
|
||||
$response = null;
|
||||
$lastError = '';
|
||||
|
||||
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
|
||||
foreach (['v1', 'v1beta'] as $ver) {
|
||||
$url = "https://generativelanguage.googleapis.com/{$ver}/models/{$this->model}:generateContent";
|
||||
try {
|
||||
$resp = Http::withHeaders(['Content-Type' => 'application/json'])
|
||||
->timeout(20)
|
||||
->post("{$url}?key={$this->apiKey}", $body);
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('[Gemini Vision] API error: ' . $response->body());
|
||||
AiUsageLog::registrar([
|
||||
'servicio' => 'gemini_vision',
|
||||
'canal' => $canal,
|
||||
'usuario_id'=> $usuarioId,
|
||||
'tiempo_ms' => $tiempoMs,
|
||||
'resultado' => 'error',
|
||||
'detalle' => ['error' => substr($response->body(), 0, 300)],
|
||||
]);
|
||||
return null;
|
||||
if ($resp->successful()) {
|
||||
$response = $resp;
|
||||
break;
|
||||
}
|
||||
$lastError = "[{$ver}] " . ($resp->json('error.message') ?? substr($resp->body(), 0, 200));
|
||||
} catch (\Throwable $e) {
|
||||
$lastError = "[{$ver}] " . $e->getMessage();
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
AiUsageLog::registrar([
|
||||
'servicio' => 'gemini_vision',
|
||||
'canal' => $canal,
|
||||
'usuario_id' => $usuarioId,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens'=> $outputTokens,
|
||||
'tiempo_ms' => $tiempoMs,
|
||||
'resultado' => $resultado ? 'ok' : 'error',
|
||||
'detalle' => $resultado ? ['banco' => $resultado['banco'], 'valor' => $resultado['valor']] : ['raw' => substr($text, 0, 200)],
|
||||
]);
|
||||
|
||||
return $resultado;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
|
||||
Log::error('[Gemini Vision] Excepcion: ' . $e->getMessage());
|
||||
AiUsageLog::registrar([
|
||||
'servicio' => 'gemini_vision',
|
||||
'canal' => $canal,
|
||||
'usuario_id'=> $usuarioId,
|
||||
'tiempo_ms' => $tiempoMs,
|
||||
'resultado' => 'error',
|
||||
'detalle' => ['error' => $e->getMessage()],
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
|
||||
|
||||
if (! $response) {
|
||||
Log::warning('[Gemini Vision] API failed: ' . $lastError);
|
||||
AiUsageLog::registrar([
|
||||
'servicio' => 'gemini_vision',
|
||||
'canal' => $canal,
|
||||
'usuario_id' => $usuarioId,
|
||||
'tiempo_ms' => $tiempoMs,
|
||||
'resultado' => 'error',
|
||||
'detalle' => ['error' => $lastError, 'modelo' => $this->model],
|
||||
]);
|
||||
// Return error info so caller can show it
|
||||
return ['__error' => $lastError];
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
AiUsageLog::registrar([
|
||||
'servicio' => 'gemini_vision',
|
||||
'canal' => $canal,
|
||||
'usuario_id' => $usuarioId,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens' => $outputTokens,
|
||||
'tiempo_ms' => $tiempoMs,
|
||||
'resultado' => $resultado ? 'ok' : 'error',
|
||||
'detalle' => $resultado
|
||||
? ['banco' => $resultado['banco'], 'valor' => $resultado['valor']]
|
||||
: ['raw' => substr($text, 0, 200), 'modelo' => $this->model],
|
||||
]);
|
||||
|
||||
return $resultado;
|
||||
}
|
||||
|
||||
private function buildPrompt(): string
|
||||
@@ -130,21 +118,13 @@ PROMPT;
|
||||
|
||||
private function parseRespuesta(string $text): ?array
|
||||
{
|
||||
$text = trim($text);
|
||||
$text = preg_replace('/```json\s*|\s*```/', '', $text);
|
||||
$text = trim($text);
|
||||
|
||||
$text = trim(preg_replace('/```json\s*|\s*```/', '', $text));
|
||||
$json = json_decode($text, true);
|
||||
|
||||
if (! is_array($json)) {
|
||||
if (! is_array($json) || isset($json['error'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($json['error'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalizar valor a entero
|
||||
if (isset($json['valor'])) {
|
||||
$json['valor'] = (int) preg_replace('/[^0-9]/', '', (string) $json['valor']);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,11 @@ class TelegramBotService
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($datosPago['__error'])) {
|
||||
$this->send($chatId, "⚠️ Error al analizar el comprobante:\n`" . $datosPago['__error'] . "`");
|
||||
return;
|
||||
}
|
||||
|
||||
$resultado = app(PagoValidadorService::class)->validar($datosPago);
|
||||
$monto = number_format($datosPago['valor'] ?? 0);
|
||||
$ref = $datosPago['referencia'] ?? 'N/A';
|
||||
|
||||
Reference in New Issue
Block a user