langchain-google-genai 2.1.12__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langchain-google-genai might be problematic. Click here for more details.

@@ -52,7 +52,8 @@ Examples:
52
52
 
53
53
  max_output_tokens: Optional[int] = Field(default=None, alias="max_tokens")
54
54
  """Maximum number of tokens to include in a candidate. Must be greater than zero.
55
- If unset, will default to ``64``."""
55
+ If unset, will use the model's default value, which varies by model.
56
+ See https://ai.google.dev/gemini-api/docs/models for model-specific limits."""
56
57
 
57
58
  n: int = 1
58
59
  """Number of chat completions to generate for each prompt. Note that the API may
@@ -0,0 +1,286 @@
1
+ """Go from v1 content blocks to generativelanguage_v1beta format."""
2
+
3
+ import json
4
+ from typing import Any, Optional, cast
5
+
6
+ from langchain_core.messages import content as types
7
+
8
+
9
+ def translate_citations_to_grounding_metadata(
10
+ citations: list[types.Citation], web_search_queries: Optional[list[str]] = None
11
+ ) -> dict[str, Any]:
12
+ """Translate LangChain Citations to Google AI grounding metadata format.
13
+
14
+ Args:
15
+ citations: List of Citation content blocks.
16
+ web_search_queries: Optional list of search queries that generated
17
+ the grounding data.
18
+
19
+ Returns:
20
+ Google AI grounding metadata dictionary.
21
+
22
+ Example:
23
+ >>> citations = [
24
+ ... create_citation(
25
+ ... url="https://uefa.com/euro2024",
26
+ ... title="UEFA Euro 2024 Results",
27
+ ... start_index=0,
28
+ ... end_index=47,
29
+ ... cited_text="Spain won the UEFA Euro 2024 championship",
30
+ ... )
31
+ ... ]
32
+ >>> metadata = translate_citations_to_grounding_metadata(citations)
33
+ >>> len(metadata["groundingChunks"])
34
+ 1
35
+ >>> metadata["groundingChunks"][0]["web"]["uri"]
36
+ 'https://uefa.com/euro2024'
37
+ """
38
+ if not citations:
39
+ return {}
40
+
41
+ # Group citations by text segment (start_index, end_index, cited_text)
42
+ segment_to_citations: dict[
43
+ tuple[Optional[int], Optional[int], Optional[str]], list[types.Citation]
44
+ ] = {}
45
+
46
+ for citation in citations:
47
+ key = (
48
+ citation.get("start_index"),
49
+ citation.get("end_index"),
50
+ citation.get("cited_text"),
51
+ )
52
+ if key not in segment_to_citations:
53
+ segment_to_citations[key] = []
54
+ segment_to_citations[key].append(citation)
55
+
56
+ # Build grounding chunks from unique URLs
57
+ url_to_chunk_index: dict[str, int] = {}
58
+ grounding_chunks: list[dict[str, Any]] = []
59
+
60
+ for citation in citations:
61
+ url = citation.get("url")
62
+ if url and url not in url_to_chunk_index:
63
+ url_to_chunk_index[url] = len(grounding_chunks)
64
+ grounding_chunks.append(
65
+ {"web": {"uri": url, "title": citation.get("title", "")}}
66
+ )
67
+
68
+ # Build grounding supports
69
+ grounding_supports: list[dict[str, Any]] = []
70
+
71
+ for (
72
+ start_index,
73
+ end_index,
74
+ cited_text,
75
+ ), citations_group in segment_to_citations.items():
76
+ if start_index is not None and end_index is not None and cited_text:
77
+ chunk_indices = []
78
+ confidence_scores = []
79
+
80
+ for citation in citations_group:
81
+ url = citation.get("url")
82
+ if url and url in url_to_chunk_index:
83
+ chunk_indices.append(url_to_chunk_index[url])
84
+
85
+ # Extract confidence scores from extras if available
86
+ extras = citation.get("extras", {})
87
+ google_metadata = extras.get("google_ai_metadata", {})
88
+ scores = google_metadata.get("confidence_scores", [])
89
+ confidence_scores.extend(scores)
90
+
91
+ support = {
92
+ "segment": {
93
+ "startIndex": start_index,
94
+ "endIndex": end_index,
95
+ "text": cited_text,
96
+ },
97
+ "groundingChunkIndices": chunk_indices,
98
+ }
99
+
100
+ if confidence_scores:
101
+ support["confidenceScores"] = confidence_scores
102
+
103
+ grounding_supports.append(support)
104
+
105
+ # Extract search queries from extras if not provided
106
+ if web_search_queries is None:
107
+ web_search_queries = []
108
+ for citation in citations:
109
+ extras = citation.get("extras", {})
110
+ google_metadata = extras.get("google_ai_metadata", {})
111
+ queries = google_metadata.get("web_search_queries", [])
112
+ web_search_queries.extend(queries)
113
+ # Remove duplicates while preserving order
114
+ web_search_queries = list(dict.fromkeys(web_search_queries))
115
+
116
+ return {
117
+ "webSearchQueries": web_search_queries,
118
+ "groundingChunks": grounding_chunks,
119
+ "groundingSupports": grounding_supports,
120
+ }
121
+
122
+
123
+ def _convert_from_v1_to_generativelanguage_v1beta(
124
+ content: list[types.ContentBlock], model_provider: str | None
125
+ ) -> list[dict[str, Any]]:
126
+ """Convert v1 content blocks to `google.ai.generativelanguage_v1beta.types.Content`.
127
+
128
+ Args:
129
+ content: List of v1 `ContentBlock` objects.
130
+ model_provider: The model provider name that generated the v1 content.
131
+
132
+ Returns:
133
+ List of dictionaries in `google.ai.generativelanguage_v1beta.types.Content`
134
+ format, ready to be sent to the API.
135
+ """
136
+ new_content: list = []
137
+ for block in content:
138
+ if not isinstance(block, dict) or "type" not in block:
139
+ continue
140
+
141
+ block_dict = dict(block) # (For typing)
142
+
143
+ # TextContentBlock
144
+ if block_dict["type"] == "text":
145
+ new_block = {"text": block_dict.get("text", "")}
146
+ new_content.append(new_block)
147
+ # Citations are only handled on output. Can't pass them back :/
148
+
149
+ # ReasoningContentBlock -> thinking
150
+ elif block_dict["type"] == "reasoning" and model_provider == "google_genai":
151
+ # Google requires passing back the thought_signature when available.
152
+ # Signatures are only provided when function calling is enabled.
153
+ if "extras" in block_dict and isinstance(block_dict["extras"], dict):
154
+ extras = block_dict["extras"]
155
+ if "signature" in extras:
156
+ new_block = {
157
+ "thought": True,
158
+ "text": block_dict.get("reasoning", ""),
159
+ "thought_signature": extras["signature"],
160
+ }
161
+ new_content.append(new_block)
162
+ # else: skip reasoning blocks without signatures
163
+ # TODO: log a warning?
164
+ # else: skip reasoning blocks without extras
165
+ # TODO: log a warning?
166
+
167
+ # ImageContentBlock
168
+ elif block_dict["type"] == "image":
169
+ if base64 := block_dict.get("base64"):
170
+ new_block = {
171
+ "inline_data": {
172
+ "mime_type": block_dict.get("mime_type", "image/jpeg"),
173
+ "data": base64.encode("utf-8")
174
+ if isinstance(base64, str)
175
+ else base64,
176
+ }
177
+ }
178
+ new_content.append(new_block)
179
+ elif url := block_dict.get("url") and model_provider == "google_genai":
180
+ # Google file service
181
+ new_block = {
182
+ "file_data": {
183
+ "mime_type": block_dict.get("mime_type", "image/jpeg"),
184
+ "file_uri": block_dict[str(url)],
185
+ }
186
+ }
187
+ new_content.append(new_block)
188
+
189
+ # TODO: AudioContentBlock -> audio once models support passing back in
190
+
191
+ # FileContentBlock (documents)
192
+ elif block_dict["type"] == "file":
193
+ if base64 := block_dict.get("base64"):
194
+ new_block = {
195
+ "inline_data": {
196
+ "mime_type": block_dict.get(
197
+ "mime_type", "application/octet-stream"
198
+ ),
199
+ "data": base64.encode("utf-8")
200
+ if isinstance(base64, str)
201
+ else base64,
202
+ }
203
+ }
204
+ new_content.append(new_block)
205
+ elif url := block_dict.get("url") and model_provider == "google_genai":
206
+ # Google file service
207
+ new_block = {
208
+ "file_data": {
209
+ "mime_type": block_dict.get(
210
+ "mime_type", "application/octet-stream"
211
+ ),
212
+ "file_uri": block_dict[str(url)],
213
+ }
214
+ }
215
+ new_content.append(new_block)
216
+
217
+ # ToolCall -> FunctionCall
218
+ elif block_dict["type"] == "tool_call":
219
+ function_call = {
220
+ "function_call": {
221
+ "name": block_dict.get("name", ""),
222
+ "args": block_dict.get("args", {}),
223
+ }
224
+ }
225
+ new_content.append(function_call)
226
+
227
+ # ToolCallChunk -> FunctionCall
228
+ elif block_dict["type"] == "tool_call_chunk":
229
+ try:
230
+ args_str = block_dict.get("args") or "{}"
231
+ input_ = json.loads(args_str) if isinstance(args_str, str) else args_str
232
+ except json.JSONDecodeError:
233
+ input_ = {}
234
+
235
+ function_call = {
236
+ "function_call": {
237
+ "name": block_dict.get("name", "no_tool_name_present"),
238
+ "args": input_,
239
+ }
240
+ }
241
+ new_content.append(function_call)
242
+
243
+ elif block_dict["type"] == "server_tool_call":
244
+ if block_dict.get("name") == "code_interpreter":
245
+ # LangChain v0 format
246
+ args = cast(dict, block_dict.get("args", {}))
247
+ executable_code = {
248
+ "type": "executable_code",
249
+ "executable_code": args.get("code", ""),
250
+ "language": args.get("language", ""),
251
+ "id": block_dict.get("id", ""),
252
+ }
253
+ # Google generativelanguage format
254
+ new_content.append(
255
+ {
256
+ "executable_code": {
257
+ "language": executable_code["language"],
258
+ "code": executable_code["executable_code"],
259
+ }
260
+ }
261
+ )
262
+
263
+ elif block_dict["type"] == "server_tool_result":
264
+ extras = cast(dict, block_dict.get("extras", {}))
265
+ if extras.get("block_type") == "code_execution_result":
266
+ # LangChain v0 format
267
+ code_execution_result = {
268
+ "type": "code_execution_result",
269
+ "code_execution_result": block_dict.get("output", ""),
270
+ "outcome": extras.get("outcome", ""),
271
+ "tool_call_id": block_dict.get("tool_call_id", ""),
272
+ }
273
+ # Google generativelanguage format
274
+ new_content.append(
275
+ {
276
+ "code_execution_result": {
277
+ "outcome": code_execution_result["outcome"],
278
+ "output": code_execution_result["code_execution_result"],
279
+ }
280
+ }
281
+ )
282
+
283
+ elif block_dict["type"] == "non_standard":
284
+ new_content.append(block_dict["value"])
285
+
286
+ return new_content
@@ -330,8 +330,10 @@ def _get_properties_from_schema(schema: Dict) -> Dict[str, Any]:
330
330
  continue
331
331
  properties_item: Dict[str, Union[str, int, Dict, List]] = {}
332
332
 
333
- # Get description from original schema before any modifications
334
- description = v.get("description")
333
+ # Preserve description and other schema properties before manipulation
334
+ original_description = v.get("description")
335
+ original_enum = v.get("enum")
336
+ original_items = v.get("items")
335
337
 
336
338
  if v.get("anyOf") and all(
337
339
  anyOf_type.get("type") != "null" for anyOf_type in v.get("anyOf", [])
@@ -354,11 +356,34 @@ def _get_properties_from_schema(schema: Dict) -> Dict[str, Any]:
354
356
  if any_of_types and item_type_ in [glm.Type.ARRAY, glm.Type.OBJECT]:
355
357
  json_type_ = "array" if item_type_ == glm.Type.ARRAY else "object"
356
358
  # Use Index -1 for consistency with `_get_nullable_type_from_schema`
357
- v = [val for val in any_of_types if val.get("type") == json_type_][-1]
359
+ filtered_schema = [
360
+ val for val in any_of_types if val.get("type") == json_type_
361
+ ][-1]
362
+ # Merge filtered schema with original properties to preserve enum/items
363
+ v = filtered_schema.copy()
364
+ if original_enum and not v.get("enum"):
365
+ v["enum"] = original_enum
366
+ if original_items and not v.get("items"):
367
+ v["items"] = original_items
368
+ elif any_of_types:
369
+ # For other types (like strings with enums), find the non-null schema
370
+ # and preserve enum/items from the original anyOf structure
371
+ non_null_schemas = [
372
+ val for val in any_of_types if val.get("type") != "null"
373
+ ]
374
+ if non_null_schemas:
375
+ filtered_schema = non_null_schemas[-1]
376
+ v = filtered_schema.copy()
377
+ if original_enum and not v.get("enum"):
378
+ v["enum"] = original_enum
379
+ if original_items and not v.get("items"):
380
+ v["items"] = original_items
358
381
 
359
382
  if v.get("enum"):
360
383
  properties_item["enum"] = v["enum"]
361
384
 
385
+ # Prefer description from the filtered schema, fall back to original
386
+ description = v.get("description") or original_description
362
387
  if description and isinstance(description, str):
363
388
  properties_item["description"] = description
364
389
 
@@ -415,6 +440,8 @@ def _get_items_from_schema(schema: Union[Dict, List, str]) -> Dict[str, Any]:
415
440
  items["description"] = (
416
441
  schema.get("description") or schema.get("title") or ""
417
442
  )
443
+ if "enum" in schema:
444
+ items["enum"] = schema["enum"]
418
445
  if _is_nullable_schema(schema):
419
446
  items["nullable"] = True
420
447
  if "required" in schema:
@@ -632,22 +632,41 @@ def generate_answer(
632
632
  )
633
633
 
634
634
 
635
- # TODO: Use candidate.finish_message when that field is launched.
636
- # For now, we derive this message from other existing fields.
637
635
  def _get_finish_message(candidate: genai.Candidate) -> str:
636
+ """Get a human-readable finish message from the candidate.
637
+
638
+ Uses the official finish_message field if available, otherwise falls back
639
+ to a manual mapping of finish reasons to descriptive messages.
640
+ """
641
+ # Use the official field when available
642
+ if hasattr(candidate, "finish_message") and candidate.finish_message:
643
+ return candidate.finish_message
644
+
645
+ # Fallback to manual mapping for all known finish reasons
638
646
  finish_messages: Dict[int, str] = {
647
+ genai.Candidate.FinishReason.STOP: "Generation completed successfully",
639
648
  genai.Candidate.FinishReason.MAX_TOKENS: (
640
649
  "Maximum token in context window reached"
641
650
  ),
642
651
  genai.Candidate.FinishReason.SAFETY: "Blocked because of safety",
643
652
  genai.Candidate.FinishReason.RECITATION: "Blocked because of recitation",
653
+ genai.Candidate.FinishReason.LANGUAGE: "Unsupported language detected",
654
+ genai.Candidate.FinishReason.BLOCKLIST: "Content hit forbidden terms",
655
+ genai.Candidate.FinishReason.PROHIBITED_CONTENT: (
656
+ "Inappropriate content detected"
657
+ ),
658
+ genai.Candidate.FinishReason.SPII: "Sensitive personal information detected",
659
+ genai.Candidate.FinishReason.IMAGE_SAFETY: "Image safety violation",
660
+ genai.Candidate.FinishReason.MALFORMED_FUNCTION_CALL: "Malformed function call",
661
+ genai.Candidate.FinishReason.UNEXPECTED_TOOL_CALL: "Unexpected tool call",
662
+ genai.Candidate.FinishReason.OTHER: "Other generation issue",
663
+ genai.Candidate.FinishReason.FINISH_REASON_UNSPECIFIED: (
664
+ "Unspecified finish reason"
665
+ ),
644
666
  }
645
667
 
646
668
  finish_reason = candidate.finish_reason
647
- if finish_reason not in finish_messages:
648
- return "Unexpected generation error"
649
-
650
- return finish_messages[finish_reason]
669
+ return finish_messages.get(finish_reason, "Unexpected generation error")
651
670
 
652
671
 
653
672
  def _convert_to_metadata(metadata: Dict[str, Any]) -> List[genai.CustomMetadata]:
@@ -58,11 +58,16 @@ from google.api_core.exceptions import (
58
58
  ResourceExhausted,
59
59
  ServiceUnavailable,
60
60
  )
61
+ from google.protobuf.json_format import MessageToDict
61
62
  from langchain_core.callbacks.manager import (
62
63
  AsyncCallbackManagerForLLMRun,
63
64
  CallbackManagerForLLMRun,
64
65
  )
65
- from langchain_core.language_models import LangSmithParams, LanguageModelInput
66
+ from langchain_core.language_models import (
67
+ LangSmithParams,
68
+ LanguageModelInput,
69
+ is_openai_data_block,
70
+ )
66
71
  from langchain_core.language_models.chat_models import BaseChatModel
67
72
  from langchain_core.messages import (
68
73
  AIMessage,
@@ -74,6 +79,7 @@ from langchain_core.messages import (
74
79
  ToolMessage,
75
80
  is_data_content_block,
76
81
  )
82
+ from langchain_core.messages import content as types
77
83
  from langchain_core.messages.ai import UsageMetadata, add_usage, subtract_usage
78
84
  from langchain_core.messages.tool import invalid_tool_call, tool_call, tool_call_chunk
79
85
  from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
@@ -110,6 +116,9 @@ from langchain_google_genai._common import (
110
116
  _BaseGoogleGenerativeAI,
111
117
  get_client_info,
112
118
  )
119
+ from langchain_google_genai._compat import (
120
+ _convert_from_v1_to_generativelanguage_v1beta,
121
+ )
113
122
  from langchain_google_genai._function_utils import (
114
123
  _dict_to_gapic_schema,
115
124
  _tool_choice_to_tool_config,
@@ -287,56 +296,78 @@ async def _achat_with_retry(generation_method: Callable, **kwargs: Any) -> Any:
287
296
  return await _achat_with_retry(**params)
288
297
 
289
298
 
290
- def _is_lc_content_block(part: dict) -> bool:
291
- return "type" in part
292
-
293
-
294
- def _is_openai_image_block(block: dict) -> bool:
295
- """Check if the block contains image data in OpenAI Chat Completions format."""
296
- if block.get("type") == "image_url":
297
- if (
298
- (set(block.keys()) <= {"type", "image_url", "detail"})
299
- and (image_url := block.get("image_url"))
300
- and isinstance(image_url, dict)
301
- ):
302
- url = image_url.get("url")
303
- if isinstance(url, str):
304
- return True
305
- else:
306
- return False
307
-
308
- return False
309
-
310
-
311
299
  def _convert_to_parts(
312
300
  raw_content: Union[str, Sequence[Union[str, dict]]],
313
301
  ) -> List[Part]:
314
- """Converts a list of LangChain messages into a Google parts."""
315
- parts = []
302
+ """Converts LangChain message content into generativelanguage_v1beta parts.
303
+
304
+ Used when preparing Human, System and AI messages for sending to the API.
305
+
306
+ Handles both legacy (pre-v1) dict-based content blocks and v1 ContentBlock objects.
307
+ """
316
308
  content = [raw_content] if isinstance(raw_content, str) else raw_content
317
309
  image_loader = ImageBytesLoader()
310
+
311
+ parts = []
312
+ # Iterate over each item in the content list, constructing a list of Parts
318
313
  for part in content:
319
314
  if isinstance(part, str):
320
315
  parts.append(Part(text=part))
321
316
  elif isinstance(part, Mapping):
322
- if _is_lc_content_block(part):
317
+ if "type" in part:
323
318
  if part["type"] == "text":
324
- parts.append(Part(text=part["text"]))
319
+ # Either old dict-style CC text block or new TextContentBlock
320
+ # Check if there's a signature attached to this text block
321
+ thought_sig = None
322
+ if "extras" in part and isinstance(part["extras"], dict):
323
+ sig = part["extras"].get("signature")
324
+ if sig and isinstance(sig, str):
325
+ # Decode base64-encoded signature back to bytes
326
+ thought_sig = base64.b64decode(sig)
327
+ if thought_sig:
328
+ parts.append(
329
+ Part(text=part["text"], thought_signature=thought_sig)
330
+ )
331
+ else:
332
+ parts.append(Part(text=part["text"]))
325
333
  elif is_data_content_block(part):
326
- if part["source_type"] == "url":
334
+ # Handle both legacy LC blocks (with `source_type`) and blocks >= v1
335
+
336
+ if "source_type" in part:
337
+ # Catch legacy v0 formats
338
+ # Safe since v1 content blocks don't have `source_type` key
339
+ if part["source_type"] == "url":
340
+ bytes_ = image_loader._bytes_from_url(part["url"])
341
+ elif part["source_type"] == "base64":
342
+ bytes_ = base64.b64decode(part["data"])
343
+ else:
344
+ # Unable to support IDContentBlock
345
+ msg = "source_type must be url or base64."
346
+ raise ValueError(msg)
347
+ elif "url" in part:
348
+ # v1 multimodal block w/ URL
327
349
  bytes_ = image_loader._bytes_from_url(part["url"])
328
- elif part["source_type"] == "base64":
329
- bytes_ = base64.b64decode(part["data"])
350
+ elif "base64" in part:
351
+ # v1 multimodal block w/ base64
352
+ bytes_ = base64.b64decode(part["base64"])
330
353
  else:
331
- msg = "source_type must be url or base64."
354
+ msg = (
355
+ "Data content block must contain 'url', 'base64', or "
356
+ "'data' field."
357
+ )
332
358
  raise ValueError(msg)
333
359
  inline_data: dict = {"data": bytes_}
334
360
  if "mime_type" in part:
335
361
  inline_data["mime_type"] = part["mime_type"]
336
362
  else:
337
- source = cast("str", part.get("url") or part.get("data"))
363
+ # Guess mime type based on data field if not provided
364
+ source = cast(
365
+ "str",
366
+ part.get("url") or part.get("base64") or part.get("data"),
367
+ )
338
368
  mime_type, _ = mimetypes.guess_type(source)
339
369
  if not mime_type:
370
+ # Last resort - try to guess based on file bytes
340
371
  kind = filetype.guess(bytes_)
341
372
  if kind:
342
373
  mime_type = kind.mime
@@ -344,6 +375,7 @@ def _convert_to_parts(
344
375
  inline_data["mime_type"] = mime_type
345
376
  parts.append(Part(inline_data=inline_data))
346
377
  elif part["type"] == "image_url":
378
+ # Chat Completions image format
347
379
  img_url = part["image_url"]
348
380
  if isinstance(img_url, dict):
349
381
  if "url" not in img_url:
@@ -351,9 +383,9 @@ def _convert_to_parts(
351
383
  raise ValueError(msg)
352
384
  img_url = img_url["url"]
353
385
  parts.append(image_loader.load_part(img_url))
354
- # Handle media type like LangChain.js
355
- # https://github.com/langchain-ai/langchainjs/blob/e536593e2585f1dd7b0afc187de4d07cb40689ba/libs/langchain-google-common/src/utils/gemini.ts#L93-L106
356
386
  elif part["type"] == "media":
387
+ # Handle `media` following pattern established in LangChain.js
388
+ # https://github.com/langchain-ai/langchainjs/blob/e536593e2585f1dd7b0afc187de4d07cb40689ba/libs/langchain-google-common/src/utils/gemini.ts#L93-L106
357
389
  if "mime_type" not in part:
358
390
  msg = f"Missing mime_type in media part: {part}"
359
391
  raise ValueError(msg)
@@ -361,10 +393,12 @@ def _convert_to_parts(
361
393
  media_part = Part()
362
394
 
363
395
  if "data" in part:
396
+ # Embedded media
364
397
  media_part.inline_data = Blob(
365
398
  data=part["data"], mime_type=mime_type
366
399
  )
367
400
  elif "file_uri" in part:
401
+ # Referenced files (e.g. stored in GCS)
368
402
  media_part.file_data = FileData(
369
403
  file_uri=part["file_uri"], mime_type=mime_type
370
404
  )
@@ -375,7 +409,59 @@ def _convert_to_parts(
375
409
  metadata = VideoMetadata(part["video_metadata"])
376
410
  media_part.video_metadata = metadata
377
411
  parts.append(media_part)
412
+ elif part["type"] == "function_call_signature":
413
+ # Signature for function_call Part - skip it here as it should be
414
+ # attached to the actual function_call Part
415
+ # This is handled separately in the history parsing logic
416
+ pass
417
+ elif part["type"] == "thinking":
418
+ # Pre-existing thinking block format that we continue to store as
419
+ thought_sig = None
420
+ if "signature" in part:
421
+ sig = part["signature"]
422
+ if sig and isinstance(sig, str):
423
+ # Decode base64-encoded signature back to bytes
424
+ thought_sig = base64.b64decode(sig)
425
+ parts.append(
426
+ Part(
427
+ text=part["thinking"],
428
+ thought=True,
429
+ thought_signature=thought_sig,
430
+ )
431
+ )
432
+ elif part["type"] == "reasoning":
433
+ # ReasoningContentBlock (when output_version = "v1")
434
+ extras = part.get("extras", {}) or {}
435
+ sig = extras.get("signature")
436
+ thought_sig = None
437
+ if sig and isinstance(sig, str):
438
+ # Decode base64-encoded signature back to bytes
439
+ thought_sig = base64.b64decode(sig)
440
+ parts.append(
441
+ Part(
442
+ text=part["reasoning"],
443
+ thought=True,
444
+ thought_signature=thought_sig,
445
+ )
446
+ )
447
+ elif part["type"] == "server_tool_call":
448
+ if part.get("name") == "code_interpreter":
449
+ args = part.get("args", {})
450
+ code = args.get("code", "")
451
+ language = args.get("language", "python")
452
+ executable_code_part = Part(
453
+ executable_code=ExecutableCode(language=language, code=code)
454
+ )
455
+ parts.append(executable_code_part)
456
+ else:
457
+ warnings.warn(
458
+ f"Server tool call with name '{part.get('name')}' is not "
459
+ "currently supported by Google GenAI. Only "
460
+ "'code_interpreter' is supported.",
461
+ stacklevel=2,
462
+ )
378
463
  elif part["type"] == "executable_code":
464
+ # Legacy executable_code format (backward compat)
379
465
  if "executable_code" not in part or "language" not in part:
380
466
  msg = (
381
467
  "Executable code part must have 'code' and 'language' "
@@ -388,7 +474,22 @@ def _convert_to_parts(
388
474
  )
389
475
  )
390
476
  parts.append(executable_code_part)
477
+ elif part["type"] == "server_tool_result":
478
+ output = part.get("output", "")
479
+ status = part.get("status", "success")
480
+ # Map status to outcome: success → 1 (OUTCOME_OK), error → 2
481
+ outcome = 1 if status == "success" else 2
482
+ # Check extras for original outcome if available
483
+ if "extras" in part and "outcome" in part["extras"]:
484
+ outcome = part["extras"]["outcome"]
485
+ code_execution_result_part = Part(
486
+ code_execution_result=CodeExecutionResult(
487
+ output=str(output), outcome=outcome
488
+ )
489
+ )
490
+ parts.append(code_execution_result_part)
391
491
  elif part["type"] == "code_execution_result":
492
+ # Legacy code_execution_result format (backward compat)
392
493
  if "code_execution_result" not in part:
393
494
  msg = (
394
495
  "Code execution result part must have "
@@ -406,24 +507,17 @@ def _convert_to_parts(
406
507
  )
407
508
  )
408
509
  parts.append(code_execution_result_part)
409
- elif part["type"] == "thinking":
410
- parts.append(Part(text=part["thinking"], thought=True))
411
510
  else:
412
- msg = (
413
- f"Unrecognized message part type: {part['type']}. Only text, "
414
- f"image_url, and media types are supported."
415
- )
511
+ msg = f"Unrecognized message part type: {part['type']}."
416
512
  raise ValueError(msg)
417
513
  else:
418
- # Yolo
514
+ # Yolo. The input message content doesn't have a `type` key
419
515
  logger.warning(
420
516
  "Unrecognized message part format. Assuming it's a text part."
421
517
  )
422
518
  parts.append(Part(text=str(part)))
423
519
  else:
424
- # TODO: Maybe some of Google's native stuff
425
- # would hit this branch.
426
- msg = "Gemini only supports text and inline_data parts."
520
+ msg = "Unknown error occurred while converting LC message content to parts."
427
521
  raise ChatGoogleGenerativeAIError(msg)
428
522
  return parts
429
523
 
@@ -441,7 +535,7 @@ def _convert_tool_message_to_parts(
441
535
  other_blocks = []
442
536
  for block in message.content:
443
537
  if isinstance(block, dict) and (
444
- is_data_content_block(block) or _is_openai_image_block(block)
538
+ is_data_content_block(block) or is_openai_data_block(block)
445
539
  ):
446
540
  media_blocks.append(block)
447
541
  else:
@@ -496,7 +590,20 @@ def _get_ai_message_tool_messages_parts(
496
590
  def _parse_chat_history(
497
591
  input_messages: Sequence[BaseMessage], convert_system_message_to_human: bool = False
498
592
  ) -> Tuple[Optional[Content], List[Content]]:
499
- messages: List[Content] = []
593
+ """Parses sequence of `BaseMessage` into system instruction and formatted messages.
594
+
595
+ Args:
596
+ input_messages: Sequence of `BaseMessage` objects representing the chat history.
597
+ convert_system_message_to_human: Whether to convert the first system message
598
+ into a human message. Deprecated, use system instructions instead.
599
+
600
+ Returns:
601
+ A tuple containing:
602
+ - An optional `google.ai.generativelanguage_v1beta.types.Content` representing
603
+ the system instruction (if any).
604
+ - A list of `google.ai.generativelanguage_v1beta.types.Content` representing the
605
+ formatted messages.
606
+ """
500
607
 
501
608
  if convert_system_message_to_human:
502
609
  warnings.warn(
@@ -505,6 +612,28 @@ def _parse_chat_history(
505
612
  DeprecationWarning,
506
613
  stacklevel=2,
507
614
  )
615
+ input_messages = list(input_messages) # Make a mutable copy
616
+
617
+ # Case where content was serialized to v1 format
618
+ for idx, message in enumerate(input_messages):
619
+ if (
620
+ isinstance(message, AIMessage)
621
+ and message.response_metadata.get("output_version") == "v1"
622
+ ):
623
+ # Unpack known v1 content to v1beta format for the request
624
+ #
625
+ # Old content types and any previously serialized messages passed back in to
626
+ # history will skip this, but hit and processed in `_convert_to_parts`
627
+ input_messages[idx] = message.model_copy(
628
+ update={
629
+ "content": _convert_from_v1_to_generativelanguage_v1beta(
630
+ cast(list[types.ContentBlock], message.content),
631
+ message.response_metadata.get("model_provider"),
632
+ )
633
+ }
634
+ )
635
+
636
+ formatted_messages: List[Content] = []
508
637
 
509
638
  system_instruction: Optional[Content] = None
510
639
  messages_without_tool_messages = [
@@ -527,19 +656,43 @@ def _parse_chat_history(
527
656
  role = "model"
528
657
  if message.tool_calls:
529
658
  ai_message_parts = []
530
- for tool_call in message.tool_calls:
659
+ # Extract any function_call_signature blocks from content
660
+ function_call_sigs: dict[int, bytes] = {}
661
+ if isinstance(message.content, list):
662
+ for idx, item in enumerate(message.content):
663
+ if (
664
+ isinstance(item, dict)
665
+ and item.get("type") == "function_call_signature"
666
+ ):
667
+ sig_str = item.get("signature", "")
668
+ if sig_str and isinstance(sig_str, str):
669
+ # Decode base64-encoded signature back to bytes
670
+ sig_bytes = base64.b64decode(sig_str)
671
+ function_call_sigs[idx] = sig_bytes
672
+
673
+ for tool_call_idx, tool_call in enumerate(message.tool_calls):
531
674
  function_call = FunctionCall(
532
675
  {
533
676
  "name": tool_call["name"],
534
677
  "args": tool_call["args"],
535
678
  }
536
679
  )
537
- ai_message_parts.append(Part(function_call=function_call))
680
+ # Check if there's a signature for this function call
681
+ # (We use the index to match signature to function call)
682
+ sig = function_call_sigs.get(tool_call_idx)
683
+ if sig:
684
+ ai_message_parts.append(
685
+ Part(function_call=function_call, thought_signature=sig)
686
+ )
687
+ else:
688
+ ai_message_parts.append(Part(function_call=function_call))
538
689
  tool_messages_parts = _get_ai_message_tool_messages_parts(
539
690
  tool_messages=tool_messages, ai_message=message
540
691
  )
541
- messages.append(Content(role=role, parts=ai_message_parts))
542
- messages.append(Content(role="user", parts=tool_messages_parts))
692
+ formatted_messages.append(Content(role=role, parts=ai_message_parts))
693
+ formatted_messages.append(
694
+ Content(role="user", parts=tool_messages_parts)
695
+ )
543
696
  continue
544
697
  if raw_function_call := message.additional_kwargs.get("function_call"):
545
698
  function_call = FunctionCall(
@@ -550,7 +703,12 @@ def _parse_chat_history(
550
703
  )
551
704
  parts = [Part(function_call=function_call)]
552
705
  else:
553
- parts = _convert_to_parts(message.content)
706
+ if message.response_metadata.get("output_version") == "v1":
707
+ # Already converted to v1beta format above
708
+ parts = message.content # type: ignore[assignment]
709
+ else:
710
+ # Prepare request content parts from message.content field
711
+ parts = _convert_to_parts(message.content)
554
712
  elif isinstance(message, HumanMessage):
555
713
  role = "user"
556
714
  parts = _convert_to_parts(message.content)
@@ -564,8 +722,11 @@ def _parse_chat_history(
564
722
  msg = f"Unexpected message with type {type(message)} at the position {i}."
565
723
  raise ValueError(msg)
566
724
 
567
- messages.append(Content(role=role, parts=parts))
568
- return system_instruction, messages
725
+ # Final step; assemble the Content object to pass to the API
726
+ # If version = "v1", the parts are already in v1beta format and will be
727
+ # automatically converted using protobuf's auto-conversion
728
+ formatted_messages.append(Content(role=role, parts=parts))
729
+ return system_instruction, formatted_messages
569
730
 
570
731
 
571
732
  # Helper function to append content consistently
@@ -589,13 +750,20 @@ def _append_to_content(
589
750
 
590
751
 
591
752
  def _parse_response_candidate(
592
- response_candidate: Candidate, streaming: bool = False
753
+ response_candidate: Candidate,
754
+ streaming: bool = False,
755
+ model_name: Optional[str] = None,
593
756
  ) -> AIMessage:
594
757
  content: Union[None, str, List[Union[str, dict]]] = None
595
758
  additional_kwargs: Dict[str, Any] = {}
759
+ response_metadata: Dict[str, Any] = {"model_provider": "google_genai"}
760
+ if model_name:
761
+ response_metadata["model_name"] = model_name
596
762
  tool_calls = []
597
763
  invalid_tool_calls = []
598
764
  tool_call_chunks = []
765
+ # Track function call signatures separately to handle them conditionally
766
+ function_call_signatures: List[dict] = []
599
767
 
600
768
  for part in response_candidate.content.parts:
601
769
  text: Optional[str] = None
@@ -608,21 +776,49 @@ def _parse_response_candidate(
608
776
  except AttributeError:
609
777
  pass
610
778
 
779
+ # Extract thought signature if present (can be on any Part type)
780
+ # Signatures are binary data, encode to base64 string for JSON serialization
781
+ thought_sig: Optional[str] = None
782
+ if hasattr(part, "thought_signature") and part.thought_signature:
783
+ try:
784
+ # Encode binary signature to base64 string
785
+ thought_sig = base64.b64encode(part.thought_signature).decode("ascii")
786
+ if not thought_sig: # Empty string
787
+ thought_sig = None
788
+ except (AttributeError, TypeError):
789
+ thought_sig = None
790
+
611
791
  if hasattr(part, "thought") and part.thought:
612
792
  thinking_message = {
613
793
  "type": "thinking",
614
794
  "thinking": part.text,
615
795
  }
796
+ # Include signature if present
797
+ if thought_sig:
798
+ thinking_message["signature"] = thought_sig
616
799
  content = _append_to_content(content, thinking_message)
617
800
  elif text is not None and text:
618
- content = _append_to_content(content, text)
801
+ # Check if this text Part has a signature attached
802
+ if thought_sig:
803
+ # Text with signature needs structured block to preserve signature
804
+ # We use a v1 TextContentBlock
805
+ text_with_sig = {
806
+ "type": "text",
807
+ "text": text,
808
+ "extras": {"signature": thought_sig},
809
+ }
810
+ content = _append_to_content(content, text_with_sig)
811
+ else:
812
+ content = _append_to_content(content, text)
619
813
 
620
814
  if hasattr(part, "executable_code") and part.executable_code is not None:
621
815
  if part.executable_code.code and part.executable_code.language:
816
+ code_id = str(uuid.uuid4()) # Generate ID if not present, needed later
622
817
  code_message = {
623
818
  "type": "executable_code",
624
819
  "executable_code": part.executable_code.code,
625
820
  "language": part.executable_code.language,
821
+ "id": code_id,
626
822
  }
627
823
  content = _append_to_content(content, code_message)
628
824
 
@@ -630,14 +826,21 @@ def _parse_response_candidate(
630
826
  hasattr(part, "code_execution_result")
631
827
  and part.code_execution_result is not None
632
828
  ) and part.code_execution_result.output:
829
+ # outcome: 1 = OUTCOME_OK (success), else = error
830
+ outcome = part.code_execution_result.outcome
633
831
  execution_result = {
634
832
  "type": "code_execution_result",
635
833
  "code_execution_result": part.code_execution_result.output,
636
- "outcome": part.code_execution_result.outcome,
834
+ "outcome": outcome,
835
+ "tool_call_id": "", # Linked via block translator
637
836
  }
638
837
  content = _append_to_content(content, execution_result)
639
838
 
640
- if part.inline_data.mime_type.startswith("audio/"):
839
+ if (
840
+ hasattr(part, "inline_data")
841
+ and part.inline_data
842
+ and part.inline_data.mime_type.startswith("audio/")
843
+ ):
641
844
  buffer = io.BytesIO()
642
845
 
643
846
  with wave.open(buffer, "wb") as wf:
@@ -647,9 +850,17 @@ def _parse_response_candidate(
647
850
  wf.setframerate(24000)
648
851
  wf.writeframes(part.inline_data.data)
649
852
 
650
- additional_kwargs["audio"] = buffer.getvalue()
853
+ audio_data = buffer.getvalue()
854
+ additional_kwargs["audio"] = audio_data
855
+
856
+ # For backwards compatibility, audio stays in additional_kwargs by default
857
+ # and is accessible via .content_blocks property
651
858
 
652
- if part.inline_data.mime_type.startswith("image/"):
859
+ if (
860
+ hasattr(part, "inline_data")
861
+ and part.inline_data
862
+ and part.inline_data.mime_type.startswith("image/")
863
+ ):
653
864
  image_format = part.inline_data.mime_type[6:]
654
865
  image_message = {
655
866
  "type": "image_url",
@@ -708,6 +919,23 @@ def _parse_response_candidate(
708
919
  id=tool_call_dict.get("id", str(uuid.uuid4())),
709
920
  )
710
921
  )
922
+
923
+ # If this function_call Part has a signature, track it separately
924
+ # We'll add it to content only if there's other content present
925
+ if thought_sig:
926
+ sig_block = {
927
+ "type": "function_call_signature",
928
+ "signature": thought_sig,
929
+ }
930
+ function_call_signatures.append(sig_block)
931
+
932
+ # Add function call signatures to content only if there's already other content
933
+ # This preserves backward compatibility where content is "" for
934
+ # function-only responses
935
+ if function_call_signatures and content is not None:
936
+ for sig_block in function_call_signatures:
937
+ content = _append_to_content(content, sig_block)
938
+
711
939
  if content is None:
712
940
  content = ""
713
941
  if isinstance(content, list) and any(
@@ -722,22 +950,100 @@ def _parse_response_candidate(
722
950
  Validate before using in production.
723
951
  """
724
952
  )
725
-
726
953
  if streaming:
727
954
  return AIMessageChunk(
728
955
  content=content,
729
956
  additional_kwargs=additional_kwargs,
957
+ response_metadata=response_metadata,
730
958
  tool_call_chunks=tool_call_chunks,
731
959
  )
732
960
 
733
961
  return AIMessage(
734
962
  content=content,
735
963
  additional_kwargs=additional_kwargs,
964
+ response_metadata=response_metadata,
736
965
  tool_calls=tool_calls,
737
966
  invalid_tool_calls=invalid_tool_calls,
738
967
  )
739
968
 
740
969
 
970
+ def _extract_grounding_metadata(candidate: Any) -> Dict[str, Any]:
971
+ """Extract grounding metadata from candidate.
972
+
973
+ core's block translator converts this metadata into citation annotations.
974
+
975
+ Uses `MessageToDict` for complete unfiltered extraction.
976
+
977
+ Falls back to custom field extraction in cases of failure for robustness.
978
+ """
979
+ if not hasattr(candidate, "grounding_metadata") or not candidate.grounding_metadata:
980
+ return {}
981
+
982
+ grounding_metadata = candidate.grounding_metadata
983
+
984
+ try:
985
+ # proto-plus wraps protobuf messages - access ._pb to get the raw protobuf
986
+ # message that MessageToDict expects
987
+ pb_message = (
988
+ grounding_metadata._pb
989
+ if hasattr(grounding_metadata, "_pb")
990
+ else grounding_metadata
991
+ )
992
+
993
+ return MessageToDict( # type: ignore[call-arg]
994
+ pb_message,
995
+ preserving_proto_field_name=True,
996
+ always_print_fields_with_no_presence=True,
997
+ # type stub issue - ensures that protobuf fields with default values
998
+ # (like start_index=0) are included in the output
999
+ )
1000
+ except (AttributeError, TypeError, ImportError):
1001
+ # Attempt manual extraction of known fields
1002
+ result: Dict[str, Any] = {}
1003
+
1004
+ # Grounding chunks
1005
+ if hasattr(grounding_metadata, "grounding_chunks"):
1006
+ grounding_chunks = []
1007
+ for chunk in grounding_metadata.grounding_chunks:
1008
+ chunk_data: Dict[str, Any] = {}
1009
+ if hasattr(chunk, "web") and chunk.web:
1010
+ chunk_data["web"] = {
1011
+ "uri": chunk.web.uri if hasattr(chunk.web, "uri") else "",
1012
+ "title": chunk.web.title if hasattr(chunk.web, "title") else "",
1013
+ }
1014
+ grounding_chunks.append(chunk_data)
1015
+ result["grounding_chunks"] = grounding_chunks
1016
+
1017
+ # Grounding supports
1018
+ if hasattr(grounding_metadata, "grounding_supports"):
1019
+ grounding_supports = []
1020
+ for support in grounding_metadata.grounding_supports:
1021
+ support_data: Dict[str, Any] = {}
1022
+ if hasattr(support, "segment") and support.segment:
1023
+ support_data["segment"] = {
1024
+ "start_index": getattr(support.segment, "start_index", 0),
1025
+ "end_index": getattr(support.segment, "end_index", 0),
1026
+ "text": getattr(support.segment, "text", ""),
1027
+ "part_index": getattr(support.segment, "part_index", 0),
1028
+ }
1029
+ if hasattr(support, "grounding_chunk_indices"):
1030
+ support_data["grounding_chunk_indices"] = list(
1031
+ support.grounding_chunk_indices
1032
+ )
1033
+ if hasattr(support, "confidence_scores"):
1034
+ support_data["confidence_scores"] = [
1035
+ round(score, 6) for score in support.confidence_scores
1036
+ ]
1037
+ grounding_supports.append(support_data)
1038
+ result["grounding_supports"] = grounding_supports
1039
+
1040
+ # Web search queries
1041
+ if hasattr(grounding_metadata, "web_search_queries"):
1042
+ result["web_search_queries"] = list(grounding_metadata.web_search_queries)
1043
+
1044
+ return result
1045
+
1046
+
741
1047
  def _response_to_result(
742
1048
  response: GenerateContentResponse,
743
1049
  stream: bool = False,
@@ -800,15 +1106,16 @@ def _response_to_result(
800
1106
  proto.Message.to_dict(safety_rating, use_integers_for_enums=False)
801
1107
  for safety_rating in candidate.safety_ratings
802
1108
  ]
803
- try:
804
- if candidate.grounding_metadata:
805
- generation_info["grounding_metadata"] = proto.Message.to_dict(
806
- candidate.grounding_metadata
807
- )
808
- except AttributeError:
809
- pass
1109
+ grounding_metadata = _extract_grounding_metadata(candidate)
1110
+ generation_info["grounding_metadata"] = grounding_metadata
810
1111
  message = _parse_response_candidate(candidate, streaming=stream)
1112
+
811
1113
  message.usage_metadata = lc_usage
1114
+
1115
+ if not hasattr(message, "response_metadata"):
1116
+ message.response_metadata = {}
1117
+ message.response_metadata["grounding_metadata"] = grounding_metadata
1118
+
812
1119
  if stream:
813
1120
  generations.append(
814
1121
  ChatGenerationChunk(
@@ -1054,7 +1361,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
1054
1361
  file = client.files.get(name=file.name)
1055
1362
 
1056
1363
  # Create cache
1057
- model = "models/gemini-1.5-flash-latest"
1364
+ model = "models/gemini-2.5-flash"
1058
1365
  cache = client.caches.create(
1059
1366
  model=model,
1060
1367
  config=types.CreateCachedContentConfig(
@@ -1110,7 +1417,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
1110
1417
  ],
1111
1418
  )
1112
1419
  ]
1113
- model = "gemini-1.5-flash-latest"
1420
+ model = "gemini-2.5-flash"
1114
1421
  cache = client.caches.create(
1115
1422
  model=model,
1116
1423
  config=CreateCachedContentConfig(
@@ -1216,8 +1523,12 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
1216
1523
  )
1217
1524
 
1218
1525
 
1526
+ # Default method uses function calling
1219
1527
  structured_llm = llm.with_structured_output(Joke)
1220
- structured_llm.invoke("Tell me a joke about cats")
1528
+
1529
+ # For more reliable output, use json_schema with native responseSchema
1530
+ structured_llm_json = llm.with_structured_output(Joke, method="json_schema")
1531
+ structured_llm_json.invoke("Tell me a joke about cats")
1221
1532
 
1222
1533
  .. code-block:: python
1223
1534
 
@@ -1227,6 +1538,18 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
1227
1538
  rating=None,
1228
1539
  )
1229
1540
 
1541
+ Two methods are supported for structured output:
1542
+
1543
+ * ``method="function_calling"`` (default): Uses tool calling to extract
1544
+ structured data. Compatible with all models.
1545
+ * ``method="json_schema"``: Uses Gemini's native structured output with
1546
+ responseSchema. More reliable but requires Gemini 1.5+ models.
1547
+ ``method="json_mode"`` also works for backwards compatibility but is a misnomer.
1548
+
1549
+ The ``json_schema`` method is recommended for better reliability as it
1550
+ constrains the model's generation process directly rather than relying on
1551
+ post-processing tool calls.
1552
+
1230
1553
  Image input:
1231
1554
  .. code-block:: python
1232
1555
 
@@ -1649,12 +1972,12 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
1649
1972
  code_execution: Optional[bool] = None,
1650
1973
  stop: Optional[list[str]] = None,
1651
1974
  **kwargs: Any,
1652
- ) -> BaseMessage:
1653
- """Enable code execution. Supported on: gemini-1.5-pro, gemini-1.5-flash,
1654
- gemini-2.0-flash, and gemini-2.0-pro. When enabled, the model can execute
1655
- code to solve problems.
1975
+ ) -> AIMessage:
1976
+ """Override invoke to add code_execution parameter.
1977
+
1978
+ Supported on: gemini-1.5-pro, gemini-1.5-flash, gemini-2.0-flash, and
1979
+ gemini-2.0-pro. When enabled, the model can execute code to solve problems.
1656
1980
  """
1657
- """Override invoke to add code_execution parameter."""
1658
1981
 
1659
1982
  if code_execution is not None:
1660
1983
  if not self._supports_code_execution:
@@ -2053,7 +2376,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
2053
2376
  ]
2054
2377
  request = GenerateContentRequest(
2055
2378
  model=self.model,
2056
- contents=history,
2379
+ contents=history, # google.ai.generativelanguage_v1beta.types.Content
2057
2380
  tools=formatted_tools,
2058
2381
  tool_config=formatted_tool_config,
2059
2382
  safety_settings=formatted_safety_settings,
@@ -2070,7 +2393,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
2070
2393
  return request
2071
2394
 
2072
2395
  def get_num_tokens(self, text: str) -> int:
2073
- """Get the number of tokens present in the text.
2396
+ """Get the number of tokens present in the text. Uses the model's tokenizer.
2074
2397
 
2075
2398
  Useful for checking if an input will fit in a model's context window.
2076
2399
 
@@ -2088,7 +2411,9 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
2088
2411
  def with_structured_output(
2089
2412
  self,
2090
2413
  schema: Union[Dict, Type[BaseModel]],
2091
- method: Optional[Literal["function_calling", "json_mode"]] = "function_calling",
2414
+ method: Optional[
2415
+ Literal["function_calling", "json_mode", "json_schema"]
2416
+ ] = "function_calling",
2092
2417
  *,
2093
2418
  include_raw: bool = False,
2094
2419
  **kwargs: Any,
@@ -2100,7 +2425,8 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
2100
2425
 
2101
2426
  parser: OutputParserLike
2102
2427
 
2103
- if method == "json_mode":
2428
+ # `json_schema` preferred, but `json_mode` kept for backwards compatibility
2429
+ if method in ("json_mode", "json_schema"):
2104
2430
  if isinstance(schema, type) and is_basemodel_subclass(schema):
2105
2431
  if issubclass(schema, BaseModelV1):
2106
2432
  schema_json = schema.schema()
@@ -2168,7 +2494,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
2168
2494
  *,
2169
2495
  tool_choice: Optional[Union[_ToolChoiceType, bool]] = None,
2170
2496
  **kwargs: Any,
2171
- ) -> Runnable[LanguageModelInput, BaseMessage]:
2497
+ ) -> Runnable[LanguageModelInput, AIMessage]:
2172
2498
  """Bind tool-like objects to this chat model.
2173
2499
 
2174
2500
  Assumes model is compatible with google-generativeAI tool-calling API.
@@ -116,22 +116,10 @@ class GoogleGenerativeAIEmbeddings(BaseModel, Embeddings):
116
116
  client_options=self.client_options,
117
117
  transport=self.transport,
118
118
  )
119
- # Only initialize async client if there's an event loop running
120
- # to avoid RuntimeError during synchronous initialization
121
- if _is_event_loop_running():
122
- # async clients don't support "rest" transport
123
- transport = self.transport
124
- if transport == "rest":
125
- transport = "grpc_asyncio"
126
- self.async_client = build_generative_async_service(
127
- credentials=self.credentials,
128
- api_key=google_api_key,
129
- client_info=client_info,
130
- client_options=self.client_options,
131
- transport=transport,
132
- )
133
- else:
134
- self.async_client = None
119
+ # Always defer async client initialization to first async call.
120
+ # Avoids implicit event loop creation and aligns with lazy init
121
+ # in chat models.
122
+ self.async_client = None
135
123
  return self
136
124
 
137
125
  @property
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langchain-google-genai
3
- Version: 2.1.12
3
+ Version: 3.0.0rc1
4
4
  Summary: An integration package connecting Google's genai package and LangChain
5
5
  License: MIT
6
6
  Project-URL: Source Code, https://github.com/langchain-ai/langchain-google/tree/main/libs/genai
7
7
  Project-URL: Release Notes, https://github.com/langchain-ai/langchain-google/releases
8
8
  Project-URL: repository, https://github.com/langchain-ai/langchain-google
9
- Requires-Python: >=3.9
10
- Requires-Dist: langchain-core>=0.3.75
11
- Requires-Dist: google-ai-generativelanguage<1,>=0.7
12
- Requires-Dist: pydantic<3,>=2
13
- Requires-Dist: filetype<2,>=1.2
9
+ Requires-Python: <4.0.0,>=3.10.0
10
+ Requires-Dist: langchain-core<2.0.0,>=1.0.0rc2
11
+ Requires-Dist: google-ai-generativelanguage<1.0.0,>=0.7.0
12
+ Requires-Dist: pydantic<3.0.0,>=2.0.0
13
+ Requires-Dist: filetype<2.0.0,>=1.2.0
14
14
  Description-Content-Type: text/markdown
15
15
 
16
16
  # langchain-google-genai
@@ -76,7 +76,7 @@ Then use the `ChatGoogleGenerativeAI` interface:
76
76
  ```python
77
77
  from langchain_google_genai import ChatGoogleGenerativeAI
78
78
 
79
- llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
79
+ llm = ChatGoogleGenerativeAI(model="gemini-flash-latest")
80
80
  response = llm.invoke("Sing a ballad of LangChain.")
81
81
  print(response.content)
82
82
  ```
@@ -97,7 +97,7 @@ Most Gemini models support image inputs.
97
97
  from langchain_core.messages import HumanMessage
98
98
  from langchain_google_genai import ChatGoogleGenerativeAI
99
99
 
100
- llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
100
+ llm = ChatGoogleGenerativeAI(model="gemini-flash-latest")
101
101
 
102
102
  message = HumanMessage(
103
103
  content=[
@@ -0,0 +1,18 @@
1
+ langchain_google_genai-3.0.0rc1.dist-info/METADATA,sha256=6JdNfUOyd0HYZ-nULDv_tXw0opP9bYVv31J4PwAO1Hc,7098
2
+ langchain_google_genai-3.0.0rc1.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ langchain_google_genai-3.0.0rc1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ langchain_google_genai-3.0.0rc1.dist-info/licenses/LICENSE,sha256=DppmdYJVSc1jd0aio6ptnMUn5tIHrdAhQ12SclEBfBg,1072
5
+ langchain_google_genai/__init__.py,sha256=yVc4wCHzajs8mvDVQLGmkxxivzApvdu7jTytrLr_i7g,2891
6
+ langchain_google_genai/_common.py,sha256=qV7EqGEsi1_BCnUDJf1ZDgOhHOIGKbgw8-D1g7Afo3g,6307
7
+ langchain_google_genai/_compat.py,sha256=QGQhorN0DHpbXMVJyKlzYdNz62L8EU6XM9EK-kfg-rQ,10979
8
+ langchain_google_genai/_enums.py,sha256=Zj3BXXLlkm_UybegCi6fLsfFhriJCt_LAJvgatgPWQ0,252
9
+ langchain_google_genai/_function_utils.py,sha256=zlH0YhFZolP6mhppBPr-7d1t6psvZcpSmkuS1oER0S0,23647
10
+ langchain_google_genai/_genai_extension.py,sha256=3W8N_vMxrwtzZvmPgmyIr6T8q6UkyS3y-IYMcr_67Ac,22597
11
+ langchain_google_genai/_image_utils.py,sha256=iJ3KrFWQhA0UwsT8ryAFAmNM7-_2ahHAppT8t9WFZGQ,5304
12
+ langchain_google_genai/chat_models.py,sha256=jVeoq3r1ekE2KXQmbEalLaMgAFxXATB3i1HDz4S1zDI,101386
13
+ langchain_google_genai/embeddings.py,sha256=MF_AoQNVhWJ6ekH6NPfZ_UGk_ixL-BX05gvZtcjYpEo,16193
14
+ langchain_google_genai/genai_aqa.py,sha256=NVW8wOWxU7T6VVshFrFlFHa5HzEPAedrgh1fOwFH7Og,4380
15
+ langchain_google_genai/google_vector_store.py,sha256=x4OcXkcYWTubu-AESNzNDJG_dbge16GeNCAOCsBoc4g,16537
16
+ langchain_google_genai/llms.py,sha256=P_e_ImBWexhKqc7LZPo6tQbwLH2-ljr6oqpE5M27TRc,5755
17
+ langchain_google_genai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ langchain_google_genai-3.0.0rc1.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- langchain_google_genai-2.1.12.dist-info/METADATA,sha256=cIpzlJRZXFdHG-aZmSE6MJUmSr-JAZzH7pDvU30nzOs,7051
2
- langchain_google_genai-2.1.12.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- langchain_google_genai-2.1.12.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- langchain_google_genai-2.1.12.dist-info/licenses/LICENSE,sha256=DppmdYJVSc1jd0aio6ptnMUn5tIHrdAhQ12SclEBfBg,1072
5
- langchain_google_genai/__init__.py,sha256=yVc4wCHzajs8mvDVQLGmkxxivzApvdu7jTytrLr_i7g,2891
6
- langchain_google_genai/_common.py,sha256=dzzcJeDVnUUI_8Y20F40SZ5NjW6RHD4xJu74naYK58k,6192
7
- langchain_google_genai/_enums.py,sha256=Zj3BXXLlkm_UybegCi6fLsfFhriJCt_LAJvgatgPWQ0,252
8
- langchain_google_genai/_function_utils.py,sha256=Pur365t4M8O5dkrnFkDc_jHiPfuI_C7ucSxvxA3tioM,22254
9
- langchain_google_genai/_genai_extension.py,sha256=LiNa7b6BZp41meJMtgaSEbaGJa59BY3UMCRMHO_CCtQ,21467
10
- langchain_google_genai/_image_utils.py,sha256=iJ3KrFWQhA0UwsT8ryAFAmNM7-_2ahHAppT8t9WFZGQ,5304
11
- langchain_google_genai/chat_models.py,sha256=G619buVU9Sef33Ubk9JL8gYLd5y_jsGMgFXeuPq7wJ0,85067
12
- langchain_google_genai/embeddings.py,sha256=Kar6nHpAJ0rWQZ0nc7_anWETlwCIovd4DhieIviGNQ8,16687
13
- langchain_google_genai/genai_aqa.py,sha256=NVW8wOWxU7T6VVshFrFlFHa5HzEPAedrgh1fOwFH7Og,4380
14
- langchain_google_genai/google_vector_store.py,sha256=x4OcXkcYWTubu-AESNzNDJG_dbge16GeNCAOCsBoc4g,16537
15
- langchain_google_genai/llms.py,sha256=P_e_ImBWexhKqc7LZPo6tQbwLH2-ljr6oqpE5M27TRc,5755
16
- langchain_google_genai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- langchain_google_genai-2.1.12.dist-info/RECORD,,