Skip to content

Commit 6b7d88d

Browse files
authored
chore(genai): Update google-genai dependency to support 2.x (#1766)
1 parent b86ee2f commit 6b7d88d

7 files changed

Lines changed: 88 additions & 47 deletions

File tree

libs/genai/langchain_google_genai/chat_models.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,16 @@ def _convert_to_parts(
394394
part_kwargs["media_resolution"] = {
395395
"level": part["media_resolution"]
396396
}
397+
thought_signature = None
397398
if "extras" in part and isinstance(part["extras"], dict):
398399
sig = part["extras"].get("signature")
399-
if sig and isinstance(sig, str):
400-
part_kwargs["thought_signature"] = base64.b64decode(sig)
400+
if isinstance(sig, str):
401+
thought_signature = base64.b64decode(sig)
402+
elif isinstance(sig, bytes):
403+
thought_signature = sig
404+
405+
if thought_signature:
406+
pass
401407

402408
parts.append(Part(**part_kwargs))
403409
elif part["type"] == "image_url":
@@ -411,11 +417,13 @@ def _convert_to_parts(
411417
# Check for thought_signature in extras
412418
# (needed for multi-turn image editing/usage)
413419
thought_sig = None
420+
image_part = image_loader.load_part(img_url)
414421
if "extras" in part and isinstance(part["extras"], dict):
415422
sig = part["extras"].get("signature")
416-
if sig and isinstance(sig, str):
417-
thought_sig = base64.b64decode(sig)
418-
image_part = image_loader.load_part(img_url)
423+
if isinstance(sig, str):
424+
image_part.thought_signature = base64.b64decode(sig)
425+
elif isinstance(sig, bytes):
426+
image_part.thought_signature = sig
419427
if thought_sig:
420428
image_part.thought_signature = thought_sig
421429
parts.append(image_part)
@@ -429,12 +437,23 @@ def _convert_to_parts(
429437
media_part_kwargs: dict[str, Any] = {}
430438

431439
if "data" in part:
432-
# Embedded media
440+
data = part["data"]
441+
if isinstance(data, str):
442+
clean_data = re.sub(r"\s+", "", data)
443+
data_validation_msg = "Data should be valid base64"
444+
if (
445+
not re.match(r"^[A-Za-z0-9+/]*={0,2}$", clean_data)
446+
or len(clean_data) % 4 != 0
447+
):
448+
raise ValueError(data_validation_msg)
449+
try:
450+
data = base64.b64decode(clean_data)
451+
except Exception:
452+
raise ValueError(data_validation_msg)
433453
media_part_kwargs["inline_data"] = Blob(
434-
data=part["data"], mime_type=mime_type
454+
data=data, mime_type=mime_type
435455
)
436456
elif "file_uri" in part:
437-
# Referenced files (e.g. stored in GCS)
438457
media_part_kwargs["file_data"] = FileData(
439458
file_uri=part["file_uri"], mime_type=mime_type
440459
)
@@ -445,6 +464,12 @@ def _convert_to_parts(
445464
_validate_video_metadata(part["video_metadata"])
446465
metadata = VideoMetadata.model_validate(part["video_metadata"])
447466
media_part_kwargs["video_metadata"] = metadata
467+
if "extras" in part and isinstance(part["extras"], dict):
468+
sig = part["extras"].get("signature")
469+
if sig and isinstance(sig, str):
470+
media_part_kwargs["thought_signature"] = (
471+
base64.b64decode(sig)
472+
)
448473

449474
if "media_resolution" in part:
450475
if model and _is_gemini_25_model(model):
@@ -461,10 +486,12 @@ def _convert_to_parts(
461486
}
462487
if "extras" in part and isinstance(part["extras"], dict):
463488
sig = part["extras"].get("signature")
464-
if sig and isinstance(sig, str):
489+
if isinstance(sig, str):
465490
media_part_kwargs["thought_signature"] = base64.b64decode(
466491
sig
467492
)
493+
elif isinstance(sig, bytes):
494+
media_part_kwargs["thought_signature"] = sig
468495

469496
parts.append(Part(**media_part_kwargs))
470497
elif part["type"] == "thinking":
@@ -993,7 +1020,7 @@ def _parse_response_candidate(
9931020
effective_model_name = model_name_for_content or model_name
9941021

9951022
parts = response_candidate.content.parts or [] if response_candidate.content else []
996-
for part in parts:
1023+
for i, part in enumerate(parts):
9971024
text: str | None = None
9981025
try:
9991026
if hasattr(part, "text") and part.text is not None:
@@ -1122,7 +1149,8 @@ def _parse_response_candidate(
11221149
)
11231150
additional_kwargs["function_call"] = function_call
11241151

1125-
tool_call_id = function_call.get("id", str(uuid.uuid4()))
1152+
raw_id = getattr(part.function_call, "id", None)
1153+
tool_call_id = str(raw_id) if raw_id else str(uuid.uuid4())
11261154
if streaming:
11271155
tool_call_chunks.append(
11281156
tool_call_chunk(

libs/genai/langchain_google_genai/embeddings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ def embed_documents(
448448
try:
449449
result = self.client.models.embed_content(
450450
model=self.model,
451-
contents=batch,
451+
contents=[{"parts": [{"text": text}]} for text in batch],
452452
config=config,
453453
)
454454
except ClientError as e:
@@ -566,7 +566,7 @@ async def aembed_documents(
566566
try:
567567
result = await self.client.aio.models.embed_content(
568568
model=self.model,
569-
contents=batch,
569+
contents=[{"parts": [{"text": text}]} for text in batch],
570570
config=config,
571571
)
572572
except ClientError as e:

libs/genai/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ version = "4.2.3"
1313
requires-python = ">=3.10.0,<4.0.0"
1414
dependencies = [
1515
"langchain-core>=1.3.2,<2.0.0",
16-
"google-genai>=1.65.0,<2.0.0",
16+
"google-genai>=1.65.0,<3.0.0",
1717
"pydantic>=2.0.0,<3.0.0",
1818
"filetype>=1.2.0,<2.0.0",
1919
]

libs/genai/tests/integration_tests/test_chat_models.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,15 @@ def test_chat_google_genai_invoke_with_image(backend_config: dict) -> None:
249249
break
250250
assert isinstance(result, AIMessage)
251251
assert isinstance(result.content, list)
252-
assert isinstance(result.content[0], str)
252+
if isinstance(result.content[0], dict):
253+
assert result.content[0].get("type") == "text"
254+
assert not result.content[0].get("text", "").startswith(" ")
255+
else:
256+
assert isinstance(result.content[0], str)
257+
assert not result.content[0].startswith(" ")
258+
253259
assert isinstance(result.content[1], dict)
254260
assert result.content[1].get("type") == "image_url"
255-
assert not result.content[0].startswith(" ")
256261
_check_usage_metadata(result)
257262

258263
# Test we can pass back in
@@ -276,7 +281,6 @@ def test_chat_google_genai_invoke_with_audio(backend_config: dict) -> None:
276281
"""Test generating audio."""
277282
# Skip on Vertex AI - having some issues possibly upstream
278283
# TODO: look later
279-
# https://discuss.ai.google.dev/t/request-allowlist-access-for-audio-output-in-gemini-2-5-pro-flash-tts-vertex-ai/108067
280284
if backend_config.get("vertexai"):
281285
pytest.skip("Gemini TTS on Vertex AI requires allowlist access")
282286

@@ -644,15 +648,24 @@ def test_chat_google_genai_invoke_thinking_disabled(backend_config: dict) -> Non
644648
"""Test invoking a thinking model with zero `thinking_budget`."""
645649
# Note certain models may not allow `thinking_budget=0`
646650
llm = ChatGoogleGenerativeAI(
647-
model="gemini-2.5-flash", thinking_budget=0, **backend_config
651+
model="gemini-3-flash-preview", thinking_budget=0, **backend_config
648652
)
649653

650654
result = llm.invoke(
651655
"How many O's are in Google? Please tell me how you double checked the result",
652656
)
653657

654658
assert isinstance(result, AIMessage)
655-
assert isinstance(result.content, str)
659+
660+
if isinstance(result.content, list):
661+
text_content = "".join(
662+
block.get("text", "")
663+
for block in result.content
664+
if isinstance(block, dict) and block.get("type") == "text"
665+
)
666+
assert len(text_content) > 0
667+
else:
668+
assert isinstance(result.content, str)
656669

657670
_check_usage_metadata(result)
658671

@@ -1470,7 +1483,7 @@ class SimpleModel(BaseModel):
14701483
# Initialize with thinking disabled
14711484
# Only certain models support disabling thinking
14721485
llm = ChatGoogleGenerativeAI(
1473-
model="gemini-2.5-flash",
1486+
model="gemini-3-flash-preview",
14741487
thinking_budget=0,
14751488
include_thoughts=False,
14761489
**backend_config,
@@ -1786,7 +1799,9 @@ class MatchResult(BaseModel):
17861799

17871800
def test_search_with_googletool(backend_config: dict) -> None:
17881801
"""Test using `GoogleTool` with Google Search."""
1789-
llm = ChatGoogleGenerativeAI(model="models/gemini-2.5-flash", **backend_config)
1802+
llm = ChatGoogleGenerativeAI(
1803+
model="models/gemini-3-flash-preview", **backend_config
1804+
)
17901805
resp = llm.invoke(
17911806
"When is the next total solar eclipse in US?",
17921807
tools=[GoogleTool(google_search={})],
@@ -1812,7 +1827,7 @@ def test_url_context_tool(backend_config: dict) -> None:
18121827

18131828
def test_google_maps_grounding(backend_config: dict) -> None:
18141829
"""Test using Google Maps grounding for location-aware responses."""
1815-
model = ChatGoogleGenerativeAI(model="gemini-2.5-flash", **backend_config)
1830+
model = ChatGoogleGenerativeAI(model="gemini-3-flash-preview", **backend_config)
18161831
model_with_maps = model.bind_tools([{"google_maps": {}}])
18171832

18181833
response = model_with_maps.invoke(
@@ -1876,7 +1891,7 @@ def test_google_maps_grounding(backend_config: dict) -> None:
18761891

18771892
def test_google_maps_grounding_invoke_direct(backend_config: dict) -> None:
18781893
"""Test passing Maps grounding tool directly to invoke without binding."""
1879-
model = ChatGoogleGenerativeAI(model="gemini-2.5-flash", **backend_config)
1894+
model = ChatGoogleGenerativeAI(model="gemini-3-flash-preview", **backend_config)
18801895

18811896
# Pass tools directly to invoke instead of binding
18821897
response = model.invoke(
@@ -1994,8 +2009,7 @@ def test_chat_google_genai_invoke_with_generation_params(backend_config: dict) -
19942009
Verifies that `max_output_tokens` (max_tokens) and `thinking_budget`
19952010
parameters passed directly to invoke() method override model defaults.
19962011
"""
1997-
# Use gemini-2.5-flash because it supports thinking_budget=0
1998-
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", **backend_config)
2012+
llm = ChatGoogleGenerativeAI(model="gemini-3-flash-preview", **backend_config)
19992013

20002014
# Test with max_output_tokens constraint
20012015
result_constrained = llm.invoke(
@@ -2519,6 +2533,7 @@ def test_context_caching(backend_config: dict) -> None:
25192533
response = chat.invoke("What is the secret number?")
25202534

25212535
assert isinstance(response, AIMessage)
2536+
25222537
text_blocks = [b for b in response.content_blocks if b["type"] == "text"]
25232538
assert any("747" in b["text"] for b in text_blocks)
25242539

libs/genai/tests/unit_tests/test_chat_models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,7 @@ def test_max_retries_parameter_handling(
14361436
},
14371437
"grounding_chunk_indices": [0],
14381438
"confidence_scores": [0.95],
1439+
"rendered_parts": None,
14391440
}
14401441
],
14411442
"web_search_queries": ["test query"],
@@ -1476,6 +1477,7 @@ def test_max_retries_parameter_handling(
14761477
},
14771478
"grounding_chunk_indices": [0],
14781479
"confidence_scores": [0.95],
1480+
"rendered_parts": None,
14791481
}
14801482
],
14811483
"image_search_queries": [],
@@ -1531,6 +1533,7 @@ def test_max_retries_parameter_handling(
15311533
},
15321534
"grounding_chunk_indices": [0],
15331535
"confidence_scores": [0.95],
1536+
"rendered_parts": None,
15341537
}
15351538
],
15361539
"web_search_queries": ["test query"],
@@ -1572,6 +1575,7 @@ def test_max_retries_parameter_handling(
15721575
},
15731576
"grounding_chunk_indices": [0],
15741577
"confidence_scores": [0.95],
1578+
"rendered_parts": None,
15751579
}
15761580
],
15771581
"image_search_queries": ["cat images"],
@@ -1660,6 +1664,7 @@ def test_grounding_metadata_to_citations_conversion() -> None:
16601664
},
16611665
"grounding_chunk_indices": [0],
16621666
"confidence_scores": [0.95],
1667+
"rendered_parts": None,
16631668
},
16641669
{
16651670
"segment": {

libs/genai/tests/unit_tests/test_embeddings.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ def test_embed_documents() -> None:
118118
mock_embed.assert_called_once()
119119
call_kwargs = mock_embed.call_args.kwargs
120120
assert call_kwargs["model"] == MODEL_NAME
121-
assert call_kwargs["contents"] == ["test text", "test text2"]
121+
assert call_kwargs["contents"] == [
122+
{"parts": [{"text": "test text"}]},
123+
{"parts": [{"text": "test text2"}]},
124+
]
122125
assert call_kwargs["config"].task_type == "RETRIEVAL_DOCUMENT"
123126

124127
# Verify the result
@@ -320,7 +323,10 @@ async def test_aembed_documents() -> None:
320323
mock_embed.assert_called_once()
321324
call_kwargs = mock_embed.call_args.kwargs
322325
assert call_kwargs["model"] == MODEL_NAME
323-
assert call_kwargs["contents"] == ["test text", "test text2"]
326+
assert call_kwargs["contents"] == [
327+
{"parts": [{"text": "test text"}]},
328+
{"parts": [{"text": "test text2"}]},
329+
]
324330
assert call_kwargs["config"].task_type == "RETRIEVAL_DOCUMENT"
325331

326332
# Verify the result

libs/genai/uv.lock

Lines changed: 7 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)