fix(scanner): gemini-2.5-flash model + actionable scan error messages

Use the same vision model as the deleted browser client, surface Gemini
quota/denial/migration failures as 502/503 with readable text, and stop
scan_attempts telemetry from blocking identification.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-27 12:41:02 -05:00
parent ecb3ee12fc
commit 7208d1403b
4 changed files with 70 additions and 17 deletions

View file

@ -361,7 +361,8 @@ export default function CameraScanner({ onCardScanned, onError }) {
}
if (!response.ok) {
throw new Error(`Scan identify failed: ${response.status}`);
const errBody = await response.json().catch(() => ({}));
throw new Error(errBody.error || `Scan identify failed: ${response.status}`);
}
const result = await response.json();

View file

@ -212,6 +212,7 @@ export async function logScanAttempt({
resultKind,
latencyMs,
}) {
try {
await sql`
INSERT INTO scan_attempts (
user_id, ocr_text, ocr_confidence, layer,
@ -226,4 +227,8 @@ export async function logScanAttempt({
${latencyMs ?? null}
)
`;
} catch (error) {
// Telemetry must not block identification (e.g. migration not yet applied).
console.error('[logScanAttempt]', error.message);
}
}

View file

@ -1,5 +1,6 @@
const DEFAULT_VISION_MODEL = process.env.GEMINI_VISION_MODEL || 'gemini-2.5-flash';
const GEMINI_MODEL =
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
`https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_VISION_MODEL}:generateContent`;
const CARD_PROMPT = `You are a specialized trading card recognition system. Analyze this image and determine if it contains a trading card (Magic: The Gathering, Pokemon, Yu-Gi-Oh, Lorcana, etc.).

View file

@ -3,6 +3,51 @@ import { checkScanRateLimit } from '../../../lib/rate-limit.js';
import { analyzeCardImage } from '../../../lib/scan-gemini.js';
import { matchCardInCatalog, logScanAttempt } from '../../../lib/card-catalog-match.js';
function scanErrorResponse(error) {
const msg = error?.message || '';
if (msg.includes('GEMINI_AI_API_KEY is not configured')) {
return {
status: 503,
body: {
error: 'Card scanning is not configured on this server (missing GEMINI_AI_API_KEY).',
},
};
}
if (msg.includes('Gemini API error: 429')) {
return {
status: 502,
body: {
error: 'Vision service quota exceeded. Check Gemini API billing or try again later.',
},
};
}
if (msg.includes('Gemini API error: 403') || msg.includes('PERMISSION_DENIED')) {
return {
status: 502,
body: {
error: 'Vision service access denied. Regenerate the Gemini API key in Google AI Studio.',
},
};
}
if (msg.includes('scan_attempts') || msg.includes('card_submissions')) {
return {
status: 503,
body: {
error: 'Scan database tables are missing. Run npm run migrate up on the deployment database.',
},
};
}
return {
status: 500,
body: { error: 'Internal server error' },
};
}
function formatCardResponse(card, ocrResult) {
return {
id: card.id,
@ -177,6 +222,7 @@ export default async function handler(req, res) {
});
} catch (error) {
console.error('[POST /api/scan/identify]', error);
return res.status(500).json({ error: 'Internal server error' });
const { status, body } = scanErrorResponse(error);
return res.status(status).json(body);
}
}