feat: Migrate Gemini SDK to google-genai (#231) #236

Merged
egullickson merged 8 commits from issue-231-migrate-gemini-sdk-google-genai into main 2026-03-01 04:08:11 +00:00
3 changed files with 60 additions and 56 deletions
Showing only changes of commit 936753fac2 - Show all commits

View File

@@ -87,7 +87,7 @@ export const vehiclesApi = {
*/ */
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 timeout: 120000 // 120 seconds for Gemini + Google Search grounding
}); });
return response.data; return response.data;
} }

View File

@@ -40,8 +40,9 @@ Return the results as a JSON object with a single "maintenanceSchedule" array.\
# VIN year code lookup: position 10 character -> base year (first cycle, 1980-2009). # VIN year code lookup: position 10 character -> base year (first cycle, 1980-2009).
# The 30-year cycle repeats: +30 for 2010-2039, +60 for 2040-2069. # The 30-year cycle repeats: +30 for 2010-2039, +60 for 2040-2069.
# Disambiguation uses position 7: numeric -> 2010+ cycle, alphabetic -> 1980s cycle. # Disambiguation uses position 7: alphabetic -> 2010+ cycle, numeric -> 1980s cycle.
# For the 2040+ cycle (when position 7 is alphabetic again), we pick the most # Per NHTSA FMVSS No. 115: MY2010+ vehicles must use alphabetic position 7.
# For the 2040+ cycle (when position 7 is numeric again), we pick the most
# recent plausible year (not more than 2 years in the future). # recent plausible year (not more than 2 years in the future).
_VIN_YEAR_CODES: dict[str, int] = { _VIN_YEAR_CODES: dict[str, int] = {
"A": 1980, "B": 1981, "C": 1982, "D": 1983, "E": 1984, "A": 1980, "B": 1981, "C": 1982, "D": 1983, "E": 1984,
@@ -58,10 +59,10 @@ def resolve_vin_year(vin: str) -> int | None:
"""Deterministically resolve model year from VIN positions 7 and 10. """Deterministically resolve model year from VIN positions 7 and 10.
VIN year codes repeat on a 30-year cycle. Position 7 disambiguates: VIN year codes repeat on a 30-year cycle. Position 7 disambiguates:
- Numeric position 7 -> 2010-2039 cycle - Alphabetic position 7 -> 2010-2039 cycle (NHTSA MY2010+ requirement)
- Alphabetic position 7 -> 1980-2009 or 2040-2050+ cycle - Numeric position 7 -> 1980-2009 or 2040-2069 cycle
For the alphabetic case with three possible cycles, picks the most recent For the numeric case with two possible cycles, picks the most recent
year that is not more than 2 years in the future. year that is not more than 2 years in the future.
Returns None if the VIN is too short or position 10 is not a valid year code. Returns None if the VIN is too short or position 10 is not a valid year code.
@@ -76,11 +77,11 @@ def resolve_vin_year(vin: str) -> int | None:
if base_year is None: if base_year is None:
return None return None
if pos7.isdigit(): if pos7.isalpha():
# Numeric position 7 -> second cycle (2010-2039) # Alphabetic position 7 -> second cycle (2010-2039)
return base_year + 30 return base_year + 30
# Alphabetic position 7 -> first cycle (1980-2009) or third cycle (2040-2069) # Numeric position 7 -> first cycle (1980-2009) or third cycle (2040-2069)
# Pick the most recent plausible year # Pick the most recent plausible year
max_plausible = datetime.now().year + 2 max_plausible = datetime.now().year + 2
@@ -381,6 +382,9 @@ class GeminiEngine:
response_mime_type="application/json", response_mime_type="application/json",
response_schema=_VIN_DECODE_SCHEMA, response_schema=_VIN_DECODE_SCHEMA,
tools=[types.Tool(google_search=types.GoogleSearch())], tools=[types.Tool(google_search=types.GoogleSearch())],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
max_remote_calls=3,
),
), ),
) )

View File

@@ -12,86 +12,86 @@ from app.engines.gemini_engine import resolve_vin_year
class TestSecondCycle: class TestSecondCycle:
"""Position 7 numeric -> 2010-2039 cycle.""" """Position 7 alphabetic -> 2010-2039 cycle (NHTSA MY2010+ requirement)."""
def test_p_with_numeric_pos7_returns_2023(self): def test_p_with_alpha_pos7_returns_2023(self):
"""P=2023 when position 7 is numeric (the bug that triggered this fix).""" """P=2023 when position 7 is alphabetic (the bug that triggered this fix)."""
# VIN: 1G1YE2D32P5602473 -- pos7='2' (numeric), pos10='P' # VIN: 1G1YE2D32P5602473 -- pos7='D' (alphabetic), pos10='P'
assert resolve_vin_year("1G1YE2D32P5602473") == 2023 assert resolve_vin_year("1G1YE2D32P5602473") == 2023
def test_a_with_numeric_pos7_returns_2010(self): def test_a_with_alpha_pos7_returns_2010(self):
"""A=2010 when position 7 is numeric.""" """A=2010 when position 7 is alphabetic."""
assert resolve_vin_year("1G1YE2112A5602473") == 2010 assert resolve_vin_year("1G1YE2D12A5602473") == 2010
def test_l_with_numeric_pos7_returns_2020(self): def test_l_with_alpha_pos7_returns_2020(self):
"""L=2020 when position 7 is numeric.""" """L=2020 when position 7 is alphabetic."""
assert resolve_vin_year("1G1YE2112L5602473") == 2020 assert resolve_vin_year("1G1YE2D12L5602473") == 2020
def test_9_with_numeric_pos7_returns_2039(self): def test_9_with_alpha_pos7_returns_2039(self):
"""9=2039 when position 7 is numeric.""" """9=2039 when position 7 is alphabetic."""
assert resolve_vin_year("1G1YE211295602473") == 2039 assert resolve_vin_year("1G1YE2D1295602473") == 2039
def test_digit_1_with_numeric_pos7_returns_2031(self): def test_digit_1_with_alpha_pos7_returns_2031(self):
"""1=2031 when position 7 is numeric.""" """1=2031 when position 7 is alphabetic."""
assert resolve_vin_year("1G1YE211215602473") == 2031 assert resolve_vin_year("1G1YE2D1215602473") == 2031
def test_s_with_numeric_pos7_returns_2025(self): def test_s_with_alpha_pos7_returns_2025(self):
"""S=2025 when position 7 is numeric.""" """S=2025 when position 7 is alphabetic."""
assert resolve_vin_year("1G1YE2112S5602473") == 2025 assert resolve_vin_year("1G1YE2D12S5602473") == 2025
def test_t_with_numeric_pos7_returns_2026(self): def test_t_with_alpha_pos7_returns_2026(self):
"""T=2026 when position 7 is numeric.""" """T=2026 when position 7 is alphabetic."""
assert resolve_vin_year("1G1YE2112T5602473") == 2026 assert resolve_vin_year("1G1YE2D12T5602473") == 2026
class TestFirstCycle: class TestFirstCycle:
"""Position 7 alphabetic -> 1980-2009 cycle (when 2040+ is not yet plausible).""" """Position 7 numeric -> 1980-2009 cycle."""
def test_m_with_alpha_pos7_returns_1991(self): def test_m_with_numeric_pos7_returns_1991(self):
"""M=1991 when position 7 is alphabetic (third cycle 2051 is not plausible).""" """M=1991 when position 7 is numeric."""
assert resolve_vin_year("1G1YE2J32M5602473") == 1991 assert resolve_vin_year("1G1YE2132M5602473") == 1991
def test_n_with_alpha_pos7_returns_1992(self): def test_n_with_numeric_pos7_returns_1992(self):
"""N=1992 when position 7 is alphabetic.""" """N=1992 when position 7 is numeric."""
assert resolve_vin_year("1G1YE2J32N5602473") == 1992 assert resolve_vin_year("1G1YE2132N5602473") == 1992
def test_p_with_alpha_pos7_returns_1993(self): def test_p_with_numeric_pos7_returns_1993(self):
"""P=1993 when position 7 is alphabetic (third cycle 2053 not plausible).""" """P=1993 when position 7 is numeric."""
assert resolve_vin_year("1G1YE2J32P5602473") == 1993 assert resolve_vin_year("1G1YE2132P5602473") == 1993
def test_y_with_alpha_pos7_returns_2000(self): def test_y_with_numeric_pos7_returns_2000(self):
"""Y=2000 when position 7 is alphabetic.""" """Y=2000 when position 7 is numeric."""
assert resolve_vin_year("1G1YE2J32Y5602473") == 2000 assert resolve_vin_year("1G1YE2132Y5602473") == 2000
class TestThirdCycle: class TestThirdCycle:
"""Position 7 alphabetic + third cycle year (2040-2050) is plausible.""" """Position 7 numeric + third cycle year (2040-2050) is plausible."""
@patch("app.engines.gemini_engine.datetime") @patch("app.engines.gemini_engine.datetime")
def test_a_with_alpha_pos7_returns_2040_when_plausible(self, mock_dt): def test_a_with_numeric_pos7_returns_2040_when_plausible(self, mock_dt):
"""A=2040 when position 7 is alphabetic and year 2040 is plausible.""" """A=2040 when position 7 is numeric and year 2040 is plausible."""
mock_dt.now.return_value = datetime(2039, 1, 1) mock_dt.now.return_value = datetime(2039, 1, 1)
# 2039 + 2 = 2041 >= 2040, so third cycle is plausible # 2039 + 2 = 2041 >= 2040, so third cycle is plausible
assert resolve_vin_year("1G1YE2J32A5602473") == 2040 assert resolve_vin_year("1G1YE2132A5602473") == 2040
@patch("app.engines.gemini_engine.datetime") @patch("app.engines.gemini_engine.datetime")
def test_l_with_alpha_pos7_returns_2050_when_plausible(self, mock_dt): def test_l_with_numeric_pos7_returns_2050_when_plausible(self, mock_dt):
"""L=2050 when position 7 is alphabetic and year 2050 is plausible.""" """L=2050 when position 7 is numeric and year 2050 is plausible."""
mock_dt.now.return_value = datetime(2049, 6, 1) mock_dt.now.return_value = datetime(2049, 6, 1)
assert resolve_vin_year("1G1YE2J32L5602473") == 2050 assert resolve_vin_year("1G1YE2132L5602473") == 2050
@patch("app.engines.gemini_engine.datetime") @patch("app.engines.gemini_engine.datetime")
def test_a_with_alpha_pos7_returns_1980_when_2040_not_plausible(self, mock_dt): def test_a_with_numeric_pos7_returns_1980_when_2040_not_plausible(self, mock_dt):
"""A=1980 when third cycle year (2040) exceeds max plausible.""" """A=1980 when third cycle year (2040) exceeds max plausible."""
mock_dt.now.return_value = datetime(2026, 2, 20) mock_dt.now.return_value = datetime(2026, 2, 20)
# 2026 + 2 = 2028 < 2040, so third cycle not plausible -> first cycle # 2026 + 2 = 2028 < 2040, so third cycle not plausible -> first cycle
assert resolve_vin_year("1G1YE2J32A5602473") == 1980 assert resolve_vin_year("1G1YE2132A5602473") == 1980
@patch("app.engines.gemini_engine.datetime") @patch("app.engines.gemini_engine.datetime")
def test_k_with_alpha_pos7_returns_2049_when_plausible(self, mock_dt): def test_k_with_numeric_pos7_returns_2049_when_plausible(self, mock_dt):
"""K=2049 when position 7 is alphabetic and year is plausible.""" """K=2049 when position 7 is numeric and year is plausible."""
mock_dt.now.return_value = datetime(2048, 1, 1) mock_dt.now.return_value = datetime(2048, 1, 1)
assert resolve_vin_year("1G1YE2J32K5602473") == 2049 assert resolve_vin_year("1G1YE2132K5602473") == 2049
class TestEdgeCases: class TestEdgeCases: