ag2 0.9.3__py3-none-any.whl → 0.9.5__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 ag2 might be problematic. Click here for more details.

Files changed (32) hide show
  1. {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/METADATA +1 -1
  2. {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/RECORD +32 -25
  3. autogen/agentchat/contrib/agent_optimizer.py +6 -3
  4. autogen/agentchat/conversable_agent.py +51 -5
  5. autogen/agentchat/group/group_utils.py +16 -7
  6. autogen/agentchat/group/guardrails.py +171 -0
  7. autogen/agentchat/group/targets/transition_target.py +10 -0
  8. autogen/agentchat/groupchat.py +93 -6
  9. autogen/agentchat/realtime/experimental/realtime_swarm.py +2 -0
  10. autogen/agents/experimental/websurfer/websurfer.py +9 -1
  11. autogen/code_utils.py +8 -6
  12. autogen/events/agent_events.py +6 -0
  13. autogen/events/helpers.py +8 -0
  14. autogen/mcp/helpers.py +45 -0
  15. autogen/mcp/mcp_proxy/mcp_proxy.py +2 -3
  16. autogen/messages/agent_messages.py +1 -1
  17. autogen/oai/client.py +44 -1
  18. autogen/oai/gemini.py +39 -24
  19. autogen/oai/gemini_types.py +1 -1
  20. autogen/oai/openai_responses.py +426 -0
  21. autogen/tools/experimental/__init__.py +4 -0
  22. autogen/tools/experimental/browser_use/browser_use.py +4 -11
  23. autogen/tools/experimental/firecrawl/__init__.py +7 -0
  24. autogen/tools/experimental/firecrawl/firecrawl_tool.py +853 -0
  25. autogen/tools/experimental/searxng/__init__.py +7 -0
  26. autogen/tools/experimental/searxng/searxng_search.py +141 -0
  27. autogen/version.py +1 -1
  28. templates/client_template/main.jinja2 +5 -2
  29. templates/main.jinja2 +1 -1
  30. {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/WHEEL +0 -0
  31. {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/licenses/LICENSE +0 -0
  32. {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/licenses/NOTICE.md +0 -0
@@ -1023,6 +1023,38 @@ class GroupChat:
1023
1023
  mentions[agent.name] = count
1024
1024
  return mentions
1025
1025
 
1026
+ def _run_input_guardrails(
1027
+ self,
1028
+ agent: "ConversableAgent",
1029
+ messages: Optional[list[dict[str, Any]]] = None,
1030
+ ) -> Optional[str]:
1031
+ """Run input guardrails for an agent before the reply is generated.
1032
+ Args:
1033
+ agent (ConversableAgent): The agent whose input guardrails to run.
1034
+ messages (Optional[list[dict[str, Any]]]): The messages to check against the guardrails.
1035
+ """
1036
+ for guardrail in agent.input_guardrails:
1037
+ guardrail_result = guardrail.check(context=messages)
1038
+
1039
+ if guardrail_result.activated:
1040
+ guardrail.target.activate_target(self)
1041
+ return f"{guardrail.activation_message}\nJustification: {guardrail_result.justification}"
1042
+ return None
1043
+
1044
+ def _run_output_guardrails(self, agent: "ConversableAgent", reply: str) -> None:
1045
+ """Run output guardrails for an agent after the reply is generated.
1046
+ Args:
1047
+ agent (ConversableAgent): The agent whose output guardrails to run.
1048
+ reply (str): The reply generated by the agent.
1049
+ """
1050
+ for guardrail in agent.output_guardrails:
1051
+ guardrail_result = guardrail.check(context=reply)
1052
+
1053
+ if guardrail_result.activated:
1054
+ guardrail.target.activate_target(self)
1055
+ return f"{guardrail.activation_message}\nJustification: {guardrail_result.justification}"
1056
+ return None
1057
+
1026
1058
 
1027
1059
  @export_module("autogen")
1028
1060
  class GroupChatManager(ConversableAgent):
@@ -1194,8 +1226,17 @@ class GroupChatManager(ConversableAgent):
1194
1226
  if not silent:
1195
1227
  iostream = IOStream.get_default()
1196
1228
  iostream.send(GroupChatRunChatEvent(speaker=speaker, silent=silent))
1197
- # let the speaker speak
1198
- reply = speaker.generate_reply(sender=self)
1229
+
1230
+ guardrails_activated = False
1231
+ guardrails_reply = groupchat._run_input_guardrails(speaker, speaker._oai_messages[self])
1232
+
1233
+ if guardrails_reply is not None:
1234
+ # if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
1235
+ guardrails_activated = True
1236
+ reply = guardrails_reply
1237
+ else:
1238
+ # let the speaker speak
1239
+ reply = speaker.generate_reply(sender=self)
1199
1240
  except KeyboardInterrupt:
1200
1241
  # let the admin agent speak if interrupted
1201
1242
  if groupchat.admin_name in groupchat.agent_names:
@@ -1215,6 +1256,15 @@ class GroupChatManager(ConversableAgent):
1215
1256
  termination_reason = "No reply generated"
1216
1257
  break
1217
1258
 
1259
+ if not guardrails_activated:
1260
+ # if the input guardrails were not activated, and the agent returned a reply
1261
+ guardrails_reply = groupchat._run_output_guardrails(speaker, reply)
1262
+
1263
+ if guardrails_reply is not None:
1264
+ # if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
1265
+ guardrails_activated = True
1266
+ reply = guardrails_reply
1267
+
1218
1268
  # check for "clear history" phrase in reply and activate clear history function if found
1219
1269
  if (
1220
1270
  groupchat.enable_clear_history
@@ -1233,7 +1283,11 @@ class GroupChatManager(ConversableAgent):
1233
1283
  a.previous_cache = None
1234
1284
 
1235
1285
  if termination_reason:
1236
- iostream.send(TerminationEvent(termination_reason=termination_reason))
1286
+ iostream.send(
1287
+ TerminationEvent(
1288
+ termination_reason=termination_reason, sender=self, recipient=speaker if speaker else None
1289
+ )
1290
+ )
1237
1291
 
1238
1292
  return True, None
1239
1293
 
@@ -1287,8 +1341,19 @@ class GroupChatManager(ConversableAgent):
1287
1341
  try:
1288
1342
  # select the next speaker
1289
1343
  speaker = await groupchat.a_select_speaker(speaker, self)
1290
- # let the speaker speak
1291
- reply = await speaker.a_generate_reply(sender=self)
1344
+ if not silent:
1345
+ iostream.send(GroupChatRunChatEvent(speaker=speaker, silent=silent))
1346
+
1347
+ guardrails_activated = False
1348
+ guardrails_reply = groupchat._run_input_guardrails(speaker, speaker._oai_messages[self])
1349
+
1350
+ if guardrails_reply is not None:
1351
+ # if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
1352
+ guardrails_activated = True
1353
+ reply = guardrails_reply
1354
+ else:
1355
+ # let the speaker speak
1356
+ reply = await speaker.a_generate_reply(sender=self)
1292
1357
  except KeyboardInterrupt:
1293
1358
  # let the admin agent speak if interrupted
1294
1359
  if groupchat.admin_name in groupchat.agent_names:
@@ -1308,6 +1373,24 @@ class GroupChatManager(ConversableAgent):
1308
1373
  termination_reason = "No reply generated"
1309
1374
  break
1310
1375
 
1376
+ if not guardrails_activated:
1377
+ # if the input guardrails were not activated, and the agent returned a reply
1378
+ guardrails_reply = groupchat._run_output_guardrails(speaker, reply)
1379
+
1380
+ if guardrails_reply is not None:
1381
+ # if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
1382
+ guardrails_activated = True
1383
+ reply = guardrails_reply
1384
+
1385
+ # check for "clear history" phrase in reply and activate clear history function if found
1386
+ if (
1387
+ groupchat.enable_clear_history
1388
+ and isinstance(reply, dict)
1389
+ and reply["content"]
1390
+ and "CLEAR HISTORY" in reply["content"].upper()
1391
+ ):
1392
+ reply["content"] = self.clear_agents_history(reply, groupchat)
1393
+
1311
1394
  # The speaker sends the message without requesting a reply
1312
1395
  await speaker.a_send(reply, self, request_reply=False, silent=silent)
1313
1396
  message = self.last_message(speaker)
@@ -1317,7 +1400,11 @@ class GroupChatManager(ConversableAgent):
1317
1400
  a.previous_cache = None
1318
1401
 
1319
1402
  if termination_reason:
1320
- iostream.send(TerminationEvent(termination_reason=termination_reason))
1403
+ iostream.send(
1404
+ TerminationEvent(
1405
+ termination_reason=termination_reason, sender=self, recipient=speaker if speaker else None
1406
+ )
1407
+ )
1321
1408
 
1322
1409
  return True, None
1323
1410
 
@@ -359,6 +359,8 @@ class SwarmableRealtimeAgent(SwarmableAgent):
359
359
  description=None,
360
360
  silent=None,
361
361
  )
362
+ self.input_guardrails = []
363
+ self.output_guardrails = []
362
364
 
363
365
  def reset_answer(self) -> None:
364
366
  """Reset the answer event."""
@@ -12,7 +12,9 @@ from ....tools.experimental import (
12
12
  BrowserUseTool,
13
13
  Crawl4AITool,
14
14
  DuckDuckGoSearchTool,
15
+ FirecrawlTool,
15
16
  PerplexitySearchTool,
17
+ SearxngSearchTool,
16
18
  TavilySearchTool,
17
19
  )
18
20
 
@@ -28,7 +30,9 @@ class WebSurferAgent(ConversableAgent):
28
30
  *,
29
31
  llm_config: Optional[Union[LLMConfig, dict[str, Any]]] = None,
30
32
  web_tool_llm_config: Optional[Union[LLMConfig, dict[str, Any]]] = None,
31
- web_tool: Literal["browser_use", "crawl4ai", "duckduckgo", "perplexity", "tavily"] = "browser_use",
33
+ web_tool: Literal[
34
+ "browser_use", "crawl4ai", "duckduckgo", "firecrawl", "perplexity", "tavily", "searxng"
35
+ ] = "browser_use",
32
36
  web_tool_kwargs: Optional[dict[str, Any]] = None,
33
37
  **kwargs: Any,
34
38
  ) -> None:
@@ -48,12 +52,16 @@ class WebSurferAgent(ConversableAgent):
48
52
  self.tool: Tool = BrowserUseTool(llm_config=web_tool_llm_config, **web_tool_kwargs) # type: ignore[arg-type]
49
53
  elif web_tool == "crawl4ai":
50
54
  self.tool = Crawl4AITool(llm_config=web_tool_llm_config, **web_tool_kwargs)
55
+ elif web_tool == "firecrawl":
56
+ self.tool = FirecrawlTool(llm_config=web_tool_llm_config, **web_tool_kwargs)
51
57
  elif web_tool == "perplexity":
52
58
  self.tool = PerplexitySearchTool(**web_tool_kwargs)
53
59
  elif web_tool == "tavily":
54
60
  self.tool = TavilySearchTool(llm_config=web_tool_llm_config, **web_tool_kwargs)
55
61
  elif web_tool == "duckduckgo":
56
62
  self.tool = DuckDuckGoSearchTool(**web_tool_kwargs)
63
+ elif web_tool == "searxng":
64
+ self.tool = SearxngSearchTool(**web_tool_kwargs)
57
65
  else:
58
66
  raise ValueError(f"Unsupported {web_tool=}.")
59
67
 
autogen/code_utils.py CHANGED
@@ -72,18 +72,20 @@ def content_str(content: Union[str, list[Union[UserMessageTextContentPart, UserM
72
72
  if not isinstance(content, list):
73
73
  raise TypeError(f"content must be None, str, or list, but got {type(content)}")
74
74
 
75
- rst = ""
75
+ rst = []
76
76
  for item in content:
77
77
  if not isinstance(item, dict):
78
78
  raise TypeError("Wrong content format: every element should be dict if the content is a list.")
79
79
  assert "type" in item, "Wrong content format. Missing 'type' key in content's dict."
80
- if item["type"] == "text":
81
- rst += item["text"]
82
- elif item["type"] == "image_url":
83
- rst += "<image>"
80
+ if item["type"] in ["text", "input_text"]:
81
+ rst.append(item["text"])
82
+ elif item["type"] in ["image_url", "input_image"]:
83
+ rst.append("<image>")
84
+ elif item["type"] in ["function", "tool_call", "tool_calls"]:
85
+ rst.append("<function>" if "name" not in item else f"<function: {item['name']}>")
84
86
  else:
85
87
  raise ValueError(f"Wrong content format: unknown type {item['type']} within the content")
86
- return rst
88
+ return "\n".join(rst)
87
89
 
88
90
 
89
91
  def infer_lang(code: str) -> str:
@@ -669,16 +669,22 @@ class TerminationEvent(BaseEvent):
669
669
  """When a workflow termination condition is met"""
670
670
 
671
671
  termination_reason: str
672
+ sender: str
673
+ recipient: Optional[str] = None
672
674
 
673
675
  def __init__(
674
676
  self,
675
677
  *,
676
678
  uuid: Optional[UUID] = None,
679
+ sender: Union["Agent", str],
680
+ recipient: Optional[Union["Agent", str]] = None,
677
681
  termination_reason: str,
678
682
  ):
679
683
  super().__init__(
680
684
  uuid=uuid,
681
685
  termination_reason=termination_reason,
686
+ sender=sender.name if hasattr(sender, "name") else sender,
687
+ recipient=recipient.name if hasattr(recipient, "name") else recipient if recipient else None,
682
688
  )
683
689
 
684
690
  def print(self, f: Optional[Callable[..., Any]] = None) -> None:
autogen/events/helpers.py CHANGED
@@ -13,12 +13,15 @@ logger = logging.getLogger(__name__)
13
13
  def deprecated_by(
14
14
  new_class: type[BaseModel],
15
15
  param_mapping: dict[str, str] = None,
16
+ default_params: dict[str, any] = None,
16
17
  ) -> Callable[[type[BaseModel]], Callable[..., BaseModel]]:
17
18
  param_mapping = param_mapping or {}
19
+ default_params = default_params or {}
18
20
 
19
21
  def decorator(
20
22
  old_class: type[BaseModel],
21
23
  param_mapping: dict[str, str] = param_mapping,
24
+ default_params: dict[str, any] = default_params,
22
25
  ) -> Callable[..., BaseModel]:
23
26
  @wraps(old_class)
24
27
  def wrapper(*args, **kwargs) -> BaseModel:
@@ -28,6 +31,11 @@ def deprecated_by(
28
31
  # Translate old parameters to new parameters
29
32
  new_kwargs = {param_mapping.get(k, k): v for k, v in kwargs.items()}
30
33
 
34
+ # Add default parameters if not already present
35
+ for key, value in default_params.items():
36
+ if key not in new_kwargs:
37
+ new_kwargs[key] = value
38
+
31
39
  # Pass the translated parameters to the new class
32
40
  return new_class(*args, **new_kwargs)
33
41
 
autogen/mcp/helpers.py ADDED
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import asyncio
5
+ import os
6
+ import signal
7
+ from asyncio.subprocess import PIPE, Process, create_subprocess_exec
8
+ from contextlib import asynccontextmanager
9
+ from typing import AsyncGenerator, Dict, Optional
10
+
11
+
12
+ @asynccontextmanager
13
+ async def run_streamable_http_client(
14
+ *, mcp_server_path: str, env_vars: Optional[Dict[str, str]] = None, startup_wait_secs: float = 5.0
15
+ ) -> AsyncGenerator[Process, None]:
16
+ """
17
+ Async context manager to run a Python subprocess for streamable-http with custom env vars.
18
+
19
+ Args:
20
+ mcp_server_path: Path to the Python script to run.
21
+ env_vars: Environment variables to export to the subprocess.
22
+ startup_wait_secs: Time to wait for the server to start (in seconds).
23
+ Yields:
24
+ An asyncio.subprocess.Process object.
25
+ """
26
+ env = os.environ.copy()
27
+ if env_vars:
28
+ env.update(env_vars)
29
+
30
+ process = await create_subprocess_exec(
31
+ "python", mcp_server_path, "streamable-http", env=env, stdout=PIPE, stderr=PIPE
32
+ )
33
+
34
+ # Optional startup delay to let the server initialize
35
+ await asyncio.sleep(startup_wait_secs)
36
+
37
+ try:
38
+ yield process
39
+ finally:
40
+ if process.returncode is None:
41
+ process.send_signal(signal.SIGINT)
42
+ try:
43
+ await asyncio.wait_for(process.wait(), timeout=5.0)
44
+ except asyncio.TimeoutError:
45
+ process.kill()
@@ -117,9 +117,8 @@ class MCPProxy:
117
117
 
118
118
  return q_params, path_params, body, security
119
119
 
120
- @property
121
- def mcp(self) -> "FastMCP":
122
- mcp = FastMCP(title=self._title)
120
+ def get_mcp(self, **settings: Any) -> "FastMCP":
121
+ mcp = FastMCP(title=self._title, **settings)
123
122
 
124
123
  for func in self._registered_funcs:
125
124
  try:
@@ -647,7 +647,7 @@ class UsingAutoReplyMessage(BaseMessage):
647
647
  f(colored("\n>>>>>>>> USING AUTO REPLY...", "red"), flush=True)
648
648
 
649
649
 
650
- @deprecated_by(TerminationEvent)
650
+ @deprecated_by(TerminationEvent, default_params={"sender": "system"})
651
651
  @wrap_message
652
652
  class TerminationMessage(BaseMessage):
653
653
  """When a workflow termination condition is met"""
autogen/oai/client.py CHANGED
@@ -51,6 +51,8 @@ if openai_result.is_successful:
51
51
  from openai.types.completion import Completion
52
52
  from openai.types.completion_usage import CompletionUsage
53
53
 
54
+ from autogen.oai.openai_responses import OpenAIResponsesClient
55
+
54
56
  if openai.__version__ >= "1.1.0":
55
57
  TOOL_ENABLED = True
56
58
  ERROR = None
@@ -314,7 +316,7 @@ class ModelClient(Protocol):
314
316
  class ModelClientResponseProtocol(Protocol):
315
317
  class Choice(Protocol):
316
318
  class Message(Protocol):
317
- content: Optional[str]
319
+ content: Optional[str] | Optional[dict[str, Any]]
318
320
 
319
321
  message: Message
320
322
 
@@ -952,6 +954,15 @@ class OpenAIWrapper:
952
954
  raise ImportError("Please install `boto3` to use the Amazon Bedrock API.")
953
955
  client = BedrockClient(response_format=response_format, **openai_config)
954
956
  self._clients.append(client)
957
+ elif api_type is not None and api_type.startswith("responses"):
958
+ # OpenAI Responses API (stateful). Reuse the same OpenAI SDK but call the `/responses` endpoint via the new client.
959
+ @require_optional_import("openai>=1.66.2", "openai")
960
+ def create_responses_client() -> "OpenAI":
961
+ client = OpenAI(**openai_config)
962
+ self._clients.append(OpenAIResponsesClient(client, response_format=response_format))
963
+ return client
964
+
965
+ client = create_responses_client()
955
966
  else:
956
967
 
957
968
  @require_optional_import("openai>=1.66.2", "openai")
@@ -1442,3 +1453,35 @@ class OpenAIWrapper:
1442
1453
  A list of text, or a list of ChatCompletion objects if function_call/tool_calls are present.
1443
1454
  """
1444
1455
  return response.message_retrieval_function(response)
1456
+
1457
+
1458
+ # -----------------------------------------------------------------------------
1459
+ # New: Responses API config entry (OpenAI-hosted preview endpoint)
1460
+ # -----------------------------------------------------------------------------
1461
+
1462
+
1463
+ @register_llm_config
1464
+ class OpenAIResponsesLLMConfigEntry(OpenAILLMConfigEntry):
1465
+ """LLMConfig entry for the OpenAI Responses API (stateful, tool-enabled).
1466
+
1467
+ This reuses all the OpenAI fields but changes *api_type* so the wrapper can
1468
+ route traffic to the `client.responses` endpoint instead of
1469
+ `chat.completions`. It inherits everything else – including reasoning
1470
+ fields – from *OpenAILLMConfigEntry* so users can simply set
1471
+
1472
+ ```python
1473
+ {
1474
+ "api_type": "responses", # <-- key differentiator
1475
+ "model": "o3", # reasoning model
1476
+ "reasoning_effort": "medium", # low / medium / high
1477
+ "stream": True,
1478
+ }
1479
+ ```
1480
+ """
1481
+
1482
+ api_type: Literal["responses"] = "responses"
1483
+ tool_choice: Optional[Literal["none", "auto", "required"]] = "auto"
1484
+ built_in_tools: Optional[list[str]] = None
1485
+
1486
+ def create_client(self) -> "ModelClient": # pragma: no cover
1487
+ raise NotImplementedError("Handled via OpenAIWrapper._register_default_client")
autogen/oai/gemini.py CHANGED
@@ -54,7 +54,6 @@ from io import BytesIO
54
54
  from typing import Any, Literal, Optional, Type, Union
55
55
 
56
56
  import requests
57
- from packaging import version
58
57
  from pydantic import BaseModel, Field
59
58
 
60
59
  from ..import_utils import optional_import_block, require_optional_import
@@ -332,6 +331,12 @@ class GeminiClient:
332
331
  recitation_part = Part(text="Unsuccessful Finish Reason: RECITATION")
333
332
  parts = [recitation_part]
334
333
  error_finish_reason = "content_filter" # As per available finish_reason in Choice
334
+ elif not response.candidates[0].content or not response.candidates[0].content.parts:
335
+ error_part = Part(
336
+ text=f"Unsuccessful Finish Reason: ({str(response.candidates[0].finish_reason)}) NO CONTENT RETURNED"
337
+ )
338
+ parts = [error_part]
339
+ error_finish_reason = "content_filter" # No other option in Choice in chat_completion.py
335
340
  else:
336
341
  parts = response.candidates[0].content.parts
337
342
  elif isinstance(response, VertexAIGenerationResponse): # or hasattr(response, "candidates"):
@@ -570,25 +575,19 @@ class GeminiClient:
570
575
 
571
576
  if part_type == "text":
572
577
  rst.append(
573
- VertexAIContent(parts=parts, role=role)
574
- if self.use_vertexai
575
- else rst.append(Content(parts=parts, role=role))
578
+ VertexAIContent(parts=parts, role=role) if self.use_vertexai else Content(parts=parts, role=role)
576
579
  )
577
- elif part_type == "tool":
578
- # Function responses should be assigned "model" role to keep them separate from function calls
579
- role = "function" if version.parse(genai.__version__) < version.parse("1.4.0") else "model"
580
+ elif part_type == "tool_call":
581
+ # Function calls should be from the model/assistant
582
+ role = "model"
580
583
  rst.append(
581
- VertexAIContent(parts=parts, role=role)
582
- if self.use_vertexai
583
- else rst.append(Content(parts=parts, role=role))
584
+ VertexAIContent(parts=parts, role=role) if self.use_vertexai else Content(parts=parts, role=role)
584
585
  )
585
- elif part_type == "tool_call":
586
- # Function calls should be assigned "user" role
587
- role = "function" if version.parse(genai.__version__) < version.parse("1.4.0") else "user"
586
+ elif part_type == "tool":
587
+ # Function responses should be from the user
588
+ role = "user"
588
589
  rst.append(
589
- VertexAIContent(parts=parts, role=role)
590
- if self.use_vertexai
591
- else rst.append(Content(parts=parts, role=role))
590
+ VertexAIContent(parts=parts, role=role) if self.use_vertexai else Content(parts=parts, role=role)
592
591
  )
593
592
  elif part_type == "image":
594
593
  # Image has multiple parts, some can be text and some can be image based
@@ -608,14 +607,14 @@ class GeminiClient:
608
607
  rst.append(
609
608
  VertexAIContent(parts=text_parts, role=role)
610
609
  if self.use_vertexai
611
- else rst.append(Content(parts=text_parts, role=role))
610
+ else Content(parts=text_parts, role=role)
612
611
  )
613
612
 
614
613
  if len(image_parts) > 0:
615
614
  rst.append(
616
615
  VertexAIContent(parts=image_parts, role=role)
617
616
  if self.use_vertexai
618
- else rst.append(Content(parts=image_parts, role=role))
617
+ else Content(parts=image_parts, role=role)
619
618
  )
620
619
 
621
620
  if len(rst) != 0 and rst[-1] is None:
@@ -908,18 +907,25 @@ def calculate_gemini_cost(use_vertexai: bool, input_tokens: int, output_tokens:
908
907
  # https://cloud.google.com/vertex-ai/generative-ai/pricing#vertex-ai-pricing
909
908
 
910
909
  if (
911
- "gemini-2.5-pro-preview-03-25" in model_name
912
- or "gemini-2.5-pro-exp-03-25" in model_name
910
+ model_name == "gemini-2.5-pro"
911
+ or "gemini-2.5-pro-preview-06-05" in model_name
913
912
  or "gemini-2.5-pro-preview-05-06" in model_name
913
+ or "gemini-2.5-pro-preview-03-25" in model_name
914
914
  ):
915
915
  if up_to_200k:
916
916
  return total_cost_mil(1.25, 10)
917
917
  else:
918
918
  return total_cost_mil(2.5, 15)
919
919
 
920
- elif "gemini-2.5-flash-preview-04-17" in model_name:
920
+ elif "gemini-2.5-flash" in model_name:
921
+ return total_cost_mil(0.3, 2.5)
922
+
923
+ elif "gemini-2.5-flash-preview-04-17" in model_name or "gemini-2.5-flash-preview-05-20" in model_name:
921
924
  return total_cost_mil(0.15, 0.6) # NON-THINKING OUTPUT PRICE, $3 FOR THINKING!
922
925
 
926
+ elif "gemini-2.5-flash-lite-preview-06-17" in model_name:
927
+ return total_cost_mil(0.1, 0.4)
928
+
923
929
  elif "gemini-2.0-flash-lite" in model_name:
924
930
  return total_cost_mil(0.075, 0.3)
925
931
 
@@ -952,9 +958,10 @@ def calculate_gemini_cost(use_vertexai: bool, input_tokens: int, output_tokens:
952
958
  # Non-Vertex AI pricing
953
959
 
954
960
  if (
955
- "gemini-2.5-pro-preview-03-25" in model_name
956
- or "gemini-2.5-pro-exp-03-25" in model_name
961
+ model_name == "gemini-2.5-pro"
962
+ or "gemini-2.5-pro-preview-06-05" in model_name
957
963
  or "gemini-2.5-pro-preview-05-06" in model_name
964
+ or "gemini-2.5-pro-preview-03-25" in model_name
958
965
  ):
959
966
  # https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview
960
967
  if up_to_200k:
@@ -962,10 +969,18 @@ def calculate_gemini_cost(use_vertexai: bool, input_tokens: int, output_tokens:
962
969
  else:
963
970
  return total_cost_mil(2.5, 15)
964
971
 
965
- elif "gemini-2.5-flash-preview-04-17" in model_name:
972
+ elif "gemini-2.5-flash" in model_name:
973
+ # https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash
974
+ return total_cost_mil(0.3, 2.5)
975
+
976
+ elif "gemini-2.5-flash-preview-04-17" in model_name or "gemini-2.5-flash-preview-05-20" in model_name:
966
977
  # https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash
967
978
  return total_cost_mil(0.15, 0.6)
968
979
 
980
+ elif "gemini-2.5-flash-lite-preview-06-17" in model_name:
981
+ # https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-lite
982
+ return total_cost_mil(0.1, 0.4)
983
+
969
984
  elif "gemini-2.0-flash-lite" in model_name:
970
985
  # https://ai.google.dev/gemini-api/docs/pricing#gemini-2.0-flash-lite
971
986
  return total_cost_mil(0.075, 0.3)
@@ -91,7 +91,7 @@ class CaseInSensitiveEnum(str, enum.Enum):
91
91
  try:
92
92
  # Creating a enum instance based on the value
93
93
  # We need to use super() to avoid infinite recursion.
94
- unknown_enum_val = super().__new__(cls, value)
94
+ unknown_enum_val = super(CaseInSensitiveEnum, cls).__new__(cls, value)
95
95
  unknown_enum_val._name_ = str(value) # pylint: disable=protected-access
96
96
  unknown_enum_val._value_ = value # pylint: disable=protected-access
97
97
  return unknown_enum_val