google-genai 1.33.0__py3-none-any.whl → 1.53.0__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.
@@ -64,6 +64,45 @@ else:
64
64
  McpTool = None
65
65
 
66
66
 
67
+ metric_name_sdk_api_map = {
68
+ 'exact_match': 'exactMatchSpec',
69
+ 'bleu': 'bleuSpec',
70
+ 'rouge_spec': 'rougeSpec',
71
+ }
72
+ metric_name_api_sdk_map = {v: k for k, v in metric_name_sdk_api_map.items()}
73
+
74
+
75
+ def _is_duck_type_of(obj: Any, cls: type[pydantic.BaseModel]) -> bool:
76
+ """Checks if an object has all of the fields of a Pydantic model.
77
+
78
+ This is a duck-typing alternative to `isinstance` to solve dual-import
79
+ problems. It returns False for dictionaries, which should be handled by
80
+ `isinstance(obj, dict)`.
81
+
82
+ Args:
83
+ obj: The object to check.
84
+ cls: The Pydantic model class to duck-type against.
85
+
86
+ Returns:
87
+ True if the object has all the fields defined in the Pydantic model, False
88
+ otherwise.
89
+ """
90
+ if isinstance(obj, dict) or not hasattr(cls, 'model_fields'):
91
+ return False
92
+
93
+ # Check if the object has all of the Pydantic model's defined fields.
94
+ all_matched = all(hasattr(obj, field) for field in cls.model_fields)
95
+ if not all_matched and isinstance(obj, pydantic.BaseModel):
96
+ # Check the other way around if obj is a Pydantic model.
97
+ # Check if the Pydantic model has all of the object's defined fields.
98
+ try:
99
+ obj_private = cls()
100
+ all_matched = all(hasattr(obj_private, f) for f in type(obj).model_fields)
101
+ except ValueError:
102
+ return False
103
+ return all_matched
104
+
105
+
67
106
  def _resource_name(
68
107
  client: _api_client.BaseApiClient,
69
108
  resource_name: str,
@@ -160,6 +199,8 @@ def _resource_name(
160
199
  def t_model(client: _api_client.BaseApiClient, model: str) -> str:
161
200
  if not model:
162
201
  raise ValueError('model is required.')
202
+ if '..' in model or '?' in model or '&' in model:
203
+ raise ValueError('invalid model parameter.')
163
204
  if client.vertexai:
164
205
  if (
165
206
  model.startswith('projects/')
@@ -276,7 +317,7 @@ def t_function_response(
276
317
  raise ValueError('function_response is required.')
277
318
  if isinstance(function_response, dict):
278
319
  return types.FunctionResponse.model_validate(function_response)
279
- elif isinstance(function_response, types.FunctionResponse):
320
+ elif _is_duck_type_of(function_response, types.FunctionResponse):
280
321
  return function_response
281
322
  else:
282
323
  raise TypeError(
@@ -309,24 +350,25 @@ def t_blobs(
309
350
 
310
351
 
311
352
  def t_blob(blob: types.BlobImageUnionDict) -> types.Blob:
312
- try:
313
- import PIL.Image
314
-
315
- PIL_Image = PIL.Image.Image
316
- except ImportError:
317
- PIL_Image = None
318
-
319
353
  if not blob:
320
354
  raise ValueError('blob is required.')
321
355
 
322
- if isinstance(blob, types.Blob):
323
- return blob
356
+ if _is_duck_type_of(blob, types.Blob):
357
+ return blob # type: ignore[return-value]
324
358
 
325
359
  if isinstance(blob, dict):
326
360
  return types.Blob.model_validate(blob)
327
361
 
328
- if PIL_Image is not None and isinstance(blob, PIL_Image):
329
- return pil_to_blob(blob)
362
+ if 'image' in blob.__class__.__name__.lower():
363
+ try:
364
+ import PIL.Image
365
+
366
+ PIL_Image = PIL.Image.Image
367
+ except ImportError:
368
+ PIL_Image = None
369
+
370
+ if PIL_Image is not None and isinstance(blob, PIL_Image):
371
+ return pil_to_blob(blob)
330
372
 
331
373
  raise TypeError(
332
374
  f'Could not parse input as Blob. Unsupported blob type: {type(blob)}'
@@ -348,27 +390,32 @@ def t_audio_blob(blob: types.BlobOrDict) -> types.Blob:
348
390
 
349
391
 
350
392
  def t_part(part: Optional[types.PartUnionDict]) -> types.Part:
351
- try:
352
- import PIL.Image
353
-
354
- PIL_Image = PIL.Image.Image
355
- except ImportError:
356
- PIL_Image = None
357
-
358
393
  if part is None:
359
394
  raise ValueError('content part is required.')
360
395
  if isinstance(part, str):
361
396
  return types.Part(text=part)
362
- if PIL_Image is not None and isinstance(part, PIL_Image):
363
- return types.Part(inline_data=pil_to_blob(part))
364
- if isinstance(part, types.File):
365
- if not part.uri or not part.mime_type:
397
+ if _is_duck_type_of(part, types.File):
398
+ if not part.uri or not part.mime_type: # type: ignore[union-attr]
366
399
  raise ValueError('file uri and mime_type are required.')
367
- return types.Part.from_uri(file_uri=part.uri, mime_type=part.mime_type)
400
+ return types.Part.from_uri(file_uri=part.uri, mime_type=part.mime_type) # type: ignore[union-attr]
368
401
  if isinstance(part, dict):
369
- return types.Part.model_validate(part)
370
- if isinstance(part, types.Part):
371
- return part
402
+ try:
403
+ return types.Part.model_validate(part)
404
+ except pydantic.ValidationError:
405
+ return types.Part(file_data=types.FileData.model_validate(part))
406
+ if _is_duck_type_of(part, types.Part):
407
+ return part # type: ignore[return-value]
408
+
409
+ if 'image' in part.__class__.__name__.lower():
410
+ try:
411
+ import PIL.Image
412
+
413
+ PIL_Image = PIL.Image.Image
414
+ except ImportError:
415
+ PIL_Image = None
416
+
417
+ if PIL_Image is not None and isinstance(part, PIL_Image):
418
+ return types.Part(inline_data=pil_to_blob(part))
372
419
  raise ValueError(f'Unsupported content part type: {type(part)}')
373
420
 
374
421
 
@@ -409,29 +456,31 @@ ContentType = Union[types.Content, types.ContentDict, types.PartUnionDict]
409
456
 
410
457
 
411
458
  def t_content(
412
- content: Optional[ContentType],
459
+ content: Union[ContentType, types.ContentDict, None],
413
460
  ) -> types.Content:
414
461
  if content is None:
415
462
  raise ValueError('content is required.')
416
- if isinstance(content, types.Content):
417
- return content
463
+ if _is_duck_type_of(content, types.Content):
464
+ return content # type: ignore[return-value]
418
465
  if isinstance(content, dict):
419
466
  try:
420
467
  return types.Content.model_validate(content)
421
468
  except pydantic.ValidationError:
422
- possible_part = types.Part.model_validate(content)
469
+ possible_part = t_part(content) # type: ignore[arg-type]
423
470
  return (
424
471
  types.ModelContent(parts=[possible_part])
425
472
  if possible_part.function_call
426
473
  else types.UserContent(parts=[possible_part])
427
474
  )
428
- if isinstance(content, types.Part):
475
+ if _is_duck_type_of(content, types.File):
476
+ return types.UserContent(parts=[t_part(content)]) # type: ignore[arg-type]
477
+ if _is_duck_type_of(content, types.Part):
429
478
  return (
430
- types.ModelContent(parts=[content])
431
- if content.function_call
432
- else types.UserContent(parts=[content])
479
+ types.ModelContent(parts=[content]) # type: ignore[arg-type]
480
+ if content.function_call # type: ignore[union-attr]
481
+ else types.UserContent(parts=[content]) # type: ignore[arg-type]
433
482
  )
434
- return types.UserContent(parts=content)
483
+ return types.UserContent(parts=content) # type: ignore[arg-type]
435
484
 
436
485
 
437
486
  def t_contents_for_embed(
@@ -470,13 +519,6 @@ def t_contents(
470
519
  if not isinstance(contents, list):
471
520
  return [t_content(contents)]
472
521
 
473
- try:
474
- import PIL.Image
475
-
476
- PIL_Image = PIL.Image.Image
477
- except ImportError:
478
- PIL_Image = None
479
-
480
522
  result: list[types.Content] = []
481
523
  accumulated_parts: list[types.Part] = []
482
524
 
@@ -485,18 +527,35 @@ def t_contents(
485
527
  ) -> TypeGuard[types.PartUnionDict]:
486
528
  if (
487
529
  isinstance(part, str)
488
- or isinstance(part, types.File)
489
- or (PIL_Image is not None and isinstance(part, PIL_Image))
490
- or isinstance(part, types.Part)
530
+ or _is_duck_type_of(part, types.File)
531
+ or _is_duck_type_of(part, types.Part)
491
532
  ):
492
533
  return True
493
534
 
494
535
  if isinstance(part, dict):
536
+ if not part:
537
+ # Empty dict should be considered as Content, not Part.
538
+ return False
495
539
  try:
496
540
  types.Part.model_validate(part)
497
541
  return True
498
542
  except pydantic.ValidationError:
499
- return False
543
+ try:
544
+ types.FileData.model_validate(part)
545
+ return True
546
+ except pydantic.ValidationError:
547
+ return False
548
+
549
+ if 'image' in part.__class__.__name__.lower():
550
+ try:
551
+ import PIL.Image
552
+
553
+ PIL_Image = PIL.Image.Image
554
+ except ImportError:
555
+ PIL_Image = None
556
+
557
+ if PIL_Image is not None and isinstance(part, PIL_Image):
558
+ return True
500
559
 
501
560
  return False
502
561
 
@@ -539,16 +598,12 @@ def t_contents(
539
598
  # append to result
540
599
  # if list, we only accept a list of types.PartUnion
541
600
  for content in contents:
542
- if (
543
- isinstance(content, types.Content)
544
- # only allowed inner list is a list of types.PartUnion
545
- or isinstance(content, list)
546
- ):
601
+ if _is_duck_type_of(content, types.Content) or isinstance(content, list):
547
602
  _append_accumulated_parts_as_content(result, accumulated_parts)
548
603
  if isinstance(content, list):
549
604
  result.append(types.UserContent(parts=content)) # type: ignore[arg-type]
550
605
  else:
551
- result.append(content)
606
+ result.append(content) # type: ignore[arg-type]
552
607
  elif _is_part(content):
553
608
  _handle_current_part(result, accumulated_parts, content)
554
609
  elif isinstance(content, dict):
@@ -840,12 +895,12 @@ def t_schema(
840
895
  return types.Schema.model_validate(origin)
841
896
  if isinstance(origin, EnumMeta):
842
897
  return _process_enum(origin, client)
843
- if isinstance(origin, types.Schema):
844
- if dict(origin) == dict(types.Schema()):
898
+ if _is_duck_type_of(origin, types.Schema):
899
+ if dict(origin) == dict(types.Schema()): # type: ignore [arg-type]
845
900
  # response_schema value was coerced to an empty Schema instance because
846
901
  # it did not adhere to the Schema field annotation
847
902
  _raise_for_unsupported_schema_type(origin)
848
- schema = origin.model_dump(exclude_unset=True)
903
+ schema = origin.model_dump(exclude_unset=True) # type: ignore[union-attr]
849
904
  process_schema(schema, client)
850
905
  return types.Schema.model_validate(schema)
851
906
 
@@ -882,8 +937,8 @@ def t_speech_config(
882
937
  ) -> Optional[types.SpeechConfig]:
883
938
  if not origin:
884
939
  return None
885
- if isinstance(origin, types.SpeechConfig):
886
- return origin
940
+ if _is_duck_type_of(origin, types.SpeechConfig):
941
+ return origin # type: ignore[return-value]
887
942
  if isinstance(origin, str):
888
943
  return types.SpeechConfig(
889
944
  voice_config=types.VoiceConfig(
@@ -899,17 +954,17 @@ def t_speech_config(
899
954
  def t_live_speech_config(
900
955
  origin: types.SpeechConfigOrDict,
901
956
  ) -> Optional[types.SpeechConfig]:
902
- if isinstance(origin, types.SpeechConfig):
957
+ if _is_duck_type_of(origin, types.SpeechConfig):
903
958
  speech_config = origin
904
959
  if isinstance(origin, dict):
905
960
  speech_config = types.SpeechConfig.model_validate(origin)
906
961
 
907
- if speech_config.multi_speaker_voice_config is not None:
962
+ if speech_config.multi_speaker_voice_config is not None: # type: ignore[union-attr]
908
963
  raise ValueError(
909
964
  'multi_speaker_voice_config is not supported in the live API.'
910
965
  )
911
966
 
912
- return speech_config
967
+ return speech_config # type: ignore[return-value]
913
968
 
914
969
 
915
970
  def t_tool(
@@ -925,7 +980,7 @@ def t_tool(
925
980
  )
926
981
  ]
927
982
  )
928
- elif McpTool is not None and isinstance(origin, McpTool):
983
+ elif McpTool is not None and _is_duck_type_of(origin, McpTool):
929
984
  return mcp_to_gemini_tool(origin)
930
985
  elif isinstance(origin, dict):
931
986
  return types.Tool.model_validate(origin)
@@ -964,32 +1019,32 @@ def t_cached_content_name(client: _api_client.BaseApiClient, name: str) -> str:
964
1019
 
965
1020
  def t_batch_job_source(
966
1021
  client: _api_client.BaseApiClient,
967
- src: Union[
968
- str, List[types.InlinedRequestOrDict], types.BatchJobSourceOrDict
969
- ],
1022
+ src: types.BatchJobSourceUnionDict,
970
1023
  ) -> types.BatchJobSource:
971
1024
  if isinstance(src, dict):
972
1025
  src = types.BatchJobSource(**src)
973
- if isinstance(src, types.BatchJobSource):
1026
+ if _is_duck_type_of(src, types.BatchJobSource):
1027
+ vertex_sources = sum(
1028
+ [src.gcs_uri is not None, src.bigquery_uri is not None] # type: ignore[union-attr]
1029
+ )
1030
+ mldev_sources = sum([
1031
+ src.inlined_requests is not None, # type: ignore[union-attr]
1032
+ src.file_name is not None, # type: ignore[union-attr]
1033
+ ])
974
1034
  if client.vertexai:
975
- if src.gcs_uri and src.bigquery_uri:
976
- raise ValueError(
977
- 'Only one of `gcs_uri` or `bigquery_uri` can be set.'
978
- )
979
- elif not src.gcs_uri and not src.bigquery_uri:
1035
+ if mldev_sources or vertex_sources != 1:
980
1036
  raise ValueError(
981
- 'One of `gcs_uri` or `bigquery_uri` must be set.'
1037
+ 'Exactly one of `gcs_uri` or `bigquery_uri` must be set, other '
1038
+ 'sources are not supported in Vertex AI.'
982
1039
  )
983
1040
  else:
984
- if src.inlined_requests and src.file_name:
1041
+ if vertex_sources or mldev_sources != 1:
985
1042
  raise ValueError(
986
- 'Only one of `inlined_requests` or `file_name` can be set.'
1043
+ 'Exactly one of `inlined_requests`, `file_name`, '
1044
+ '`inlined_embed_content_requests`, or `embed_content_file_name` '
1045
+ 'must be set, other sources are not supported in Gemini API.'
987
1046
  )
988
- elif not src.inlined_requests and not src.file_name:
989
- raise ValueError(
990
- 'One of `inlined_requests` or `file_name` must be set.'
991
- )
992
- return src
1047
+ return src # type: ignore[return-value]
993
1048
 
994
1049
  elif isinstance(src, list):
995
1050
  return types.BatchJobSource(inlined_requests=src)
@@ -1012,6 +1067,29 @@ def t_batch_job_source(
1012
1067
  raise ValueError(f'Unsupported source: {src}')
1013
1068
 
1014
1069
 
1070
+ def t_embedding_batch_job_source(
1071
+ client: _api_client.BaseApiClient,
1072
+ src: types.EmbeddingsBatchJobSourceOrDict,
1073
+ ) -> types.EmbeddingsBatchJobSource:
1074
+ if isinstance(src, dict):
1075
+ src = types.EmbeddingsBatchJobSource(**src)
1076
+
1077
+ if _is_duck_type_of(src, types.EmbeddingsBatchJobSource):
1078
+ mldev_sources = sum([
1079
+ src.inlined_requests is not None,
1080
+ src.file_name is not None,
1081
+ ])
1082
+ if mldev_sources != 1:
1083
+ raise ValueError(
1084
+ 'Exactly one of `inlined_requests`, `file_name`, '
1085
+ '`inlined_embed_content_requests`, or `embed_content_file_name` '
1086
+ 'must be set, other sources are not supported in Gemini API.'
1087
+ )
1088
+ return src
1089
+ else:
1090
+ raise ValueError(f'Unsupported source type: {type(src)}')
1091
+
1092
+
1015
1093
  def t_batch_job_destination(
1016
1094
  dest: Union[str, types.BatchJobDestinationOrDict],
1017
1095
  ) -> types.BatchJobDestination:
@@ -1031,12 +1109,29 @@ def t_batch_job_destination(
1031
1109
  )
1032
1110
  else:
1033
1111
  raise ValueError(f'Unsupported destination: {dest}')
1034
- elif isinstance(dest, types.BatchJobDestination):
1112
+ elif _is_duck_type_of(dest, types.BatchJobDestination):
1035
1113
  return dest
1036
1114
  else:
1037
1115
  raise ValueError(f'Unsupported destination: {dest}')
1038
1116
 
1039
1117
 
1118
+ def t_recv_batch_job_destination(dest: dict[str, Any]) -> dict[str, Any]:
1119
+ # Rename inlinedResponses if it looks like an embedding response.
1120
+ inline_responses = dest.get('inlinedResponses', {}).get(
1121
+ 'inlinedResponses', []
1122
+ )
1123
+ if not inline_responses:
1124
+ return dest
1125
+ for response in inline_responses:
1126
+ inner_response = response.get('response', {})
1127
+ if not inner_response:
1128
+ continue
1129
+ if 'embedding' in inner_response:
1130
+ dest['inlinedEmbedContentResponses'] = dest.pop('inlinedResponses')
1131
+ break
1132
+ return dest
1133
+
1134
+
1040
1135
  def t_batch_job_name(client: _api_client.BaseApiClient, name: str) -> str:
1041
1136
  if not client.vertexai:
1042
1137
  mldev_pattern = r'batches/[^/]+$'
@@ -1060,12 +1155,16 @@ def t_job_state(state: str) -> str:
1060
1155
  return 'JOB_STATE_UNSPECIFIED'
1061
1156
  elif state == 'BATCH_STATE_PENDING':
1062
1157
  return 'JOB_STATE_PENDING'
1158
+ elif state == 'BATCH_STATE_RUNNING':
1159
+ return 'JOB_STATE_RUNNING'
1063
1160
  elif state == 'BATCH_STATE_SUCCEEDED':
1064
1161
  return 'JOB_STATE_SUCCEEDED'
1065
1162
  elif state == 'BATCH_STATE_FAILED':
1066
1163
  return 'JOB_STATE_FAILED'
1067
1164
  elif state == 'BATCH_STATE_CANCELLED':
1068
1165
  return 'JOB_STATE_CANCELLED'
1166
+ elif state == 'BATCH_STATE_EXPIRED':
1167
+ return 'JOB_STATE_EXPIRED'
1069
1168
  else:
1070
1169
  return state
1071
1170
 
@@ -1110,13 +1209,13 @@ def t_file_name(
1110
1209
  name: Optional[Union[str, types.File, types.Video, types.GeneratedVideo]],
1111
1210
  ) -> str:
1112
1211
  # Remove the files/ prefix since it's added to the url path.
1113
- if isinstance(name, types.File):
1114
- name = name.name
1115
- elif isinstance(name, types.Video):
1116
- name = name.uri
1117
- elif isinstance(name, types.GeneratedVideo):
1118
- if name.video is not None:
1119
- name = name.video.uri
1212
+ if _is_duck_type_of(name, types.File):
1213
+ name = name.name # type: ignore[union-attr]
1214
+ elif _is_duck_type_of(name, types.Video):
1215
+ name = name.uri # type: ignore[union-attr]
1216
+ elif _is_duck_type_of(name, types.GeneratedVideo):
1217
+ if name.video is not None: # type: ignore[union-attr]
1218
+ name = name.video.uri # type: ignore[union-attr]
1120
1219
  else:
1121
1220
  name = None
1122
1221
 
@@ -1159,7 +1258,7 @@ def t_tuning_job_status(status: str) -> Union[types.JobState, str]:
1159
1258
  def t_content_strict(content: types.ContentOrDict) -> types.Content:
1160
1259
  if isinstance(content, dict):
1161
1260
  return types.Content.model_validate(content)
1162
- elif isinstance(content, types.Content):
1261
+ elif _is_duck_type_of(content, types.Content):
1163
1262
  return content
1164
1263
  else:
1165
1264
  raise ValueError(
@@ -1250,10 +1349,14 @@ def t_metrics(
1250
1349
 
1251
1350
  elif hasattr(metric, 'prompt_template') and metric.prompt_template:
1252
1351
  pointwise_spec = {'metric_prompt_template': metric.prompt_template}
1253
- system_instruction = getv(metric, ['judge_model_system_instruction'])
1352
+ system_instruction = getv(
1353
+ metric, ['judge_model_system_instruction']
1354
+ )
1254
1355
  if system_instruction:
1255
1356
  pointwise_spec['system_instruction'] = system_instruction
1256
- return_raw_output = getv(metric, ['return_raw_output'])
1357
+ return_raw_output = getv(
1358
+ metric, ['return_raw_output']
1359
+ )
1257
1360
  if return_raw_output:
1258
1361
  pointwise_spec['custom_output_format_config'] = { # type: ignore[assignment]
1259
1362
  'return_raw_output': return_raw_output