Compare commits

...

5 Commits

Author SHA1 Message Date
Eric Gullickson
3b5b84729f fix: increase VIN decode timeout to 60s for Gemini cold start (refs #229)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Default 10s API client timeout caused frontend "Failed to decode" errors
when Gemini engine cold-starts (34s+ on first call).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:30:31 -06:00
Eric Gullickson
781241966c chore: change google region
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 38s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
2026-02-19 20:59:40 -06:00
Eric Gullickson
bf6742f6ea chore: Gemini 3.0 Flash Preview model
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 33s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
2026-02-19 20:36:34 -06:00
Eric Gullickson
5bb44be8bc chore: Change to Gemini 3.0 Flash
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 36s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 21s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
2026-02-19 20:35:06 -06:00
Eric Gullickson
361f58d7c6 fix: resolve VIN decode cache race, fuzzy matching, and silent failure (refs #229)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Prevent lower-confidence Gemini results from overwriting higher-confidence
cache entries, add reverse-contains matching so values like "X5 xDrive35i"
match DB option "X5", and show amber hint when dropdown matching fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 20:14:54 -06:00
5 changed files with 56 additions and 7 deletions

View File

@@ -648,11 +648,12 @@ export class VehiclesService {
engine_type = EXCLUDED.engine_type, engine_type = EXCLUDED.engine_type,
body_type = EXCLUDED.body_type, body_type = EXCLUDED.body_type,
raw_data = EXCLUDED.raw_data, raw_data = EXCLUDED.raw_data,
cached_at = NOW()`, cached_at = NOW()
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response)] WHERE (vin_cache.raw_data->>'confidence')::float <= $8`,
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response), response.confidence ?? 1]
); );
logger.debug('VIN cached', { vin }); logger.debug('VIN cached', { vin, confidence: response.confidence });
} catch (error) { } catch (error) {
logger.error('Failed to cache VIN data', { vin, error }); logger.error('Failed to cache VIN data', { vin, error });
// Don't throw - caching failure shouldn't break the decode flow // Don't throw - caching failure shouldn't break the decode flow
@@ -741,6 +742,12 @@ export class VehiclesService {
const sourceEngine = response.engine; const sourceEngine = response.engine;
const sourceTransmission = response.transmission; const sourceTransmission = response.transmission;
logger.debug('VIN decode raw values', {
vin: response.vin,
year: sourceYear, make: sourceMake, model: sourceModel,
trim: sourceTrim, confidence: response.confidence
});
// Year is always high confidence if present (exact numeric match) // Year is always high confidence if present (exact numeric match)
const year: MatchedField<number> = { const year: MatchedField<number> = {
value: sourceYear, value: sourceYear,
@@ -854,6 +861,26 @@ export class VehiclesService {
return { value: containsMatch, sourceValue, confidence: 'medium' }; return { value: containsMatch, sourceValue, confidence: 'medium' };
} }
// Try reverse contains - source value contains option (e.g., source "X5 xDrive35i" contains option "X5")
// Prefer the longest matching option to avoid false positives (e.g., "X5 M" over "X5")
const reverseMatches = options.filter(opt => {
const normalizedOpt = opt.toLowerCase().trim();
return normalizedSource.includes(normalizedOpt) && normalizedOpt.length > 0;
});
if (reverseMatches.length > 0) {
const bestMatch = reverseMatches.reduce((a, b) => a.length >= b.length ? a : b);
return { value: bestMatch, sourceValue, confidence: 'medium' };
}
// Try word-start match - source starts with option + separator (e.g., "X5 xDrive" starts with "X5 ")
const wordStartMatch = options.find(opt => {
const normalizedOpt = opt.toLowerCase().trim();
return normalizedSource.startsWith(normalizedOpt + ' ') || normalizedSource.startsWith(normalizedOpt + '-');
});
if (wordStartMatch) {
return { value: wordStartMatch, sourceValue, confidence: 'medium' };
}
// No match found - return source value as hint with no match // No match found - return source value as hint with no match
return { value: null, sourceValue, confidence: 'none' }; return { value: null, sourceValue, confidence: 'none' };
} }

View File

@@ -206,8 +206,8 @@ services:
VISION_MONTHLY_LIMIT: "1000" VISION_MONTHLY_LIMIT: "1000"
# Vertex AI / Gemini configuration (maintenance schedule extraction) # Vertex AI / Gemini configuration (maintenance schedule extraction)
VERTEX_AI_PROJECT: motovaultpro VERTEX_AI_PROJECT: motovaultpro
VERTEX_AI_LOCATION: us-central1 VERTEX_AI_LOCATION: global
GEMINI_MODEL: gemini-2.5-flash GEMINI_MODEL: gemini-3-flash-preview
volumes: volumes:
- /tmp/vin-debug:/tmp/vin-debug - /tmp/vin-debug:/tmp/vin-debug
- ./secrets/app/auth0-ocr-client-id.txt:/run/secrets/auth0-ocr-client-id:ro - ./secrets/app/auth0-ocr-client-id.txt:/run/secrets/auth0-ocr-client-id:ro

View File

@@ -86,7 +86,9 @@ export const vehiclesApi = {
* Requires Pro or Enterprise tier * Requires Pro or Enterprise tier
*/ */
decodeVin: async (vin: string): Promise<DecodedVehicleData> => { decodeVin: async (vin: string): Promise<DecodedVehicleData> => {
const response = await apiClient.post('/vehicles/decode-vin', { vin }); const response = await apiClient.post('/vehicles/decode-vin', { vin }, {
timeout: 60000 // 60 seconds for Gemini cold start
});
return response.data; return response.data;
} }
}; };

View File

@@ -114,6 +114,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const [isDecoding, setIsDecoding] = useState(false); const [isDecoding, setIsDecoding] = useState(false);
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
const [decodeError, setDecodeError] = useState<string | null>(null); const [decodeError, setDecodeError] = useState<string | null>(null);
const [decodeHint, setDecodeHint] = useState<string | null>(null);
// VIN OCR capture hook // VIN OCR capture hook
const vinOcr = useVinOcr(); const vinOcr = useVinOcr();
@@ -524,6 +525,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
setIsDecoding(true); setIsDecoding(true);
setDecodeError(null); setDecodeError(null);
setDecodeHint(null);
try { try {
const decoded = await vehiclesApi.decodeVin(vin); const decoded = await vehiclesApi.decodeVin(vin);
@@ -588,6 +590,21 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
setValue('transmission', decoded.transmission.value); setValue('transmission', decoded.transmission.value);
} }
// Check if decode returned data but matching failed for key fields
const hasMatchedValue = decoded.year.value || decoded.make.value || decoded.model.value;
const hasSourceValue = decoded.year.sourceValue || decoded.make.sourceValue || decoded.model.sourceValue;
if (!hasMatchedValue && hasSourceValue) {
const parts = [
decoded.year.sourceValue,
decoded.make.sourceValue,
decoded.model.sourceValue,
decoded.trimLevel.sourceValue
].filter(Boolean);
setDecodeHint(
`Could not match VIN data to dropdowns. Decoded as: ${parts.join(' ')}. Please select values manually.`
);
}
setLoadingDropdowns(false); setLoadingDropdowns(false);
isVinDecoding.current = false; isVinDecoding.current = false;
} catch (error: any) { } catch (error: any) {
@@ -671,6 +688,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
{decodeError && ( {decodeError && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{decodeError}</p> <p className="mt-1 text-sm text-red-600 dark:text-red-400">{decodeError}</p>
)} )}
{decodeHint && (
<p className="mt-1 text-sm text-amber-600 dark:text-amber-400">{decodeHint}</p>
)}
{vinOcr.error && ( {vinOcr.error && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{vinOcr.error}</p> <p className="mt-1 text-sm text-red-600 dark:text-red-400">{vinOcr.error}</p>
)} )}

View File

@@ -32,7 +32,7 @@ class Settings:
# Vertex AI / Gemini configuration # Vertex AI / Gemini configuration
self.vertex_ai_project: str = os.getenv("VERTEX_AI_PROJECT", "") self.vertex_ai_project: str = os.getenv("VERTEX_AI_PROJECT", "")
self.vertex_ai_location: str = os.getenv( self.vertex_ai_location: str = os.getenv(
"VERTEX_AI_LOCATION", "us-central1" "VERTEX_AI_LOCATION", "global"
) )
self.gemini_model: str = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") self.gemini_model: str = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")