ag2 0.9.5__py3-none-any.whl → 0.9.7__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.

@@ -0,0 +1,134 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import subprocess
6
+ from abc import ABC, abstractmethod
7
+ from contextvars import ContextVar
8
+ from typing import Any, Optional
9
+
10
+ __all__ = ["PythonEnvironment"]
11
+
12
+
13
+ class PythonEnvironment(ABC):
14
+ """Python execution environments base class"""
15
+
16
+ # Shared context variable for tracking the current environment
17
+ _current_python_environment: ContextVar["PythonEnvironment"] = ContextVar("_current_python_environment")
18
+
19
+ def __init__(self):
20
+ """
21
+ Initialize the Python environment.
22
+ """
23
+ self._token = None
24
+
25
+ # Set up the environment
26
+ self._setup_environment()
27
+
28
+ def __enter__(self):
29
+ """
30
+ Enter the environment context.
31
+ Sets this environment as the current one.
32
+ """
33
+ # Set this as the current Python environment in the context
34
+ self._token = PythonEnvironment._current_python_environment.set(self)
35
+
36
+ return self
37
+
38
+ def __exit__(self, exc_type, exc_val, exc_tb):
39
+ """
40
+ Exit the environment context.
41
+ Resets the current environment and performs cleanup.
42
+ """
43
+ # Reset the context variable if this was the active environment
44
+ if self._token is not None:
45
+ PythonEnvironment._current_python_environment.reset(self._token)
46
+ self._token = None
47
+
48
+ # Clean up resources
49
+ self._cleanup_environment()
50
+
51
+ @abstractmethod
52
+ def _setup_environment(self) -> None:
53
+ """Set up the Python environment. Called by __enter__."""
54
+ pass
55
+
56
+ @abstractmethod
57
+ def _cleanup_environment(self) -> None:
58
+ """Clean up the Python environment. Called by __exit__."""
59
+ pass
60
+
61
+ @abstractmethod
62
+ def get_executable(self) -> str:
63
+ """
64
+ Get the path to the Python executable in this environment.
65
+
66
+ Returns:
67
+ The full path to the Python executable.
68
+ """
69
+ pass
70
+
71
+ @abstractmethod
72
+ async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
73
+ """
74
+ Execute the given code in this environment.
75
+
76
+ Args:
77
+ code: The Python code to execute.
78
+ script_path: Path where the code should be saved before execution.
79
+ timeout: Maximum execution time in seconds.
80
+
81
+ Returns:
82
+ dict with execution results including stdout, stderr, and success status.
83
+ """
84
+ pass
85
+
86
+ # Utility method for subclasses to wrap (for async support)
87
+ def _write_to_file(self, script_path: str, content: str) -> None:
88
+ """
89
+ Write content to a file (blocking operation).
90
+
91
+ This is a helper method for use with asyncify in async contexts.
92
+
93
+ Args:
94
+ script_path: Path to the file to write.
95
+ content: Content to write to the file.
96
+ """
97
+ with open(script_path, "w") as f:
98
+ f.write(content)
99
+
100
+ # Utility method for subclasses to wrap (for async support)
101
+ def _run_subprocess(self, cmd: list[str], timeout: int) -> subprocess.CompletedProcess:
102
+ """
103
+ Run a subprocess (blocking operation).
104
+
105
+ This is a helper method for use with asyncify in async contexts.
106
+
107
+ Args:
108
+ cmd: Command to run as a list of strings.
109
+ timeout: Maximum execution time in seconds.
110
+
111
+ Returns:
112
+ CompletedProcess instance with results of the subprocess.
113
+ """
114
+ return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
115
+
116
+ @classmethod
117
+ def get_current_python_environment(
118
+ cls, python_environment: Optional["PythonEnvironment"] = None
119
+ ) -> Optional["PythonEnvironment"]:
120
+ """
121
+ Get the current Python environment or the specified one if provided.
122
+
123
+ Args:
124
+ python_environment: Optional environment to return if specified.
125
+
126
+ Returns:
127
+ The current Python environment or None if none is active.
128
+ """
129
+ if python_environment is not None:
130
+ return python_environment
131
+ try:
132
+ return cls._current_python_environment.get()
133
+ except LookupError:
134
+ return None
@@ -0,0 +1,86 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from typing import Any, Optional
10
+
11
+ from asyncer import asyncify
12
+
13
+ from .python_environment import PythonEnvironment
14
+
15
+ __all__ = ["SystemPythonEnvironment"]
16
+
17
+
18
+ class SystemPythonEnvironment(PythonEnvironment):
19
+ """A Python environment using the system's Python installation."""
20
+
21
+ def __init__(
22
+ self,
23
+ executable: Optional[str] = None,
24
+ ):
25
+ """
26
+ Initialize a system Python environment.
27
+
28
+ Args:
29
+ executable: Optional path to a specific Python executable. If None, uses the current Python executable.
30
+ """
31
+ self._executable = executable or sys.executable
32
+ super().__init__()
33
+
34
+ def _setup_environment(self) -> None:
35
+ """Set up the system Python environment."""
36
+ # Verify the Python executable exists
37
+ if not os.path.exists(self._executable):
38
+ raise RuntimeError(f"Python executable not found at: {self._executable}")
39
+
40
+ logging.info(f"Using system Python at: {self._executable}")
41
+
42
+ def _cleanup_environment(self) -> None:
43
+ """Clean up the system Python environment."""
44
+ # No cleanup needed for system Python
45
+ pass
46
+
47
+ def get_executable(self) -> str:
48
+ """Get the path to the Python executable."""
49
+ return self._executable
50
+
51
+ async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
52
+ """Execute code using the system Python."""
53
+ try:
54
+ # Get the Python executable
55
+ python_executable = self.get_executable()
56
+
57
+ # Verify the executable exists
58
+ if not os.path.exists(python_executable):
59
+ return {"success": False, "error": f"Python executable not found at {python_executable}"}
60
+
61
+ # Ensure the directory for the script exists
62
+ script_dir = os.path.dirname(script_path)
63
+ if script_dir:
64
+ os.makedirs(script_dir, exist_ok=True)
65
+
66
+ # Write the code to the script file using asyncify (from base class)
67
+ await asyncify(self._write_to_file)(script_path, code)
68
+
69
+ logging.info(f"Wrote code to {script_path}")
70
+
71
+ try:
72
+ # Execute directly with subprocess using asyncify for better reliability
73
+ result = await asyncify(self._run_subprocess)([python_executable, script_path], timeout)
74
+
75
+ # Main execution result
76
+ return {
77
+ "success": result.returncode == 0,
78
+ "stdout": result.stdout,
79
+ "stderr": result.stderr,
80
+ "returncode": result.returncode,
81
+ }
82
+ except subprocess.TimeoutExpired:
83
+ return {"success": False, "error": f"Execution timed out after {timeout} seconds"}
84
+
85
+ except Exception as e:
86
+ return {"success": False, "error": f"Execution error: {str(e)}"}
@@ -0,0 +1,224 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from typing import Any, Optional
11
+
12
+ from asyncer import asyncify
13
+
14
+ from .python_environment import PythonEnvironment
15
+
16
+ __all__ = ["VenvPythonEnvironment"]
17
+
18
+
19
+ class VenvPythonEnvironment(PythonEnvironment):
20
+ """A Python environment using a virtual environment (venv)."""
21
+
22
+ def __init__(
23
+ self,
24
+ python_version: Optional[str] = None,
25
+ python_path: Optional[str] = None,
26
+ venv_path: Optional[str] = None,
27
+ ):
28
+ """
29
+ Initialize a virtual environment for Python execution.
30
+
31
+ If you pass in a venv_path the path will be checked for a valid venv. If the venv doesn't exist it will be created using the python_version or python_path provided.
32
+
33
+ If the python_version or python_path is provided and the venv_path is not, a temporary directory will be created for venv and it will be setup with the provided python version.
34
+
35
+ If python_path is provided, it will take precedence over python_version.
36
+
37
+ The python version will not be installed if it doesn't exist and a RuntimeError will be raised.
38
+
39
+ Args:
40
+ python_version: The Python version to use (e.g., "3.11"), otherwise defaults to the current executing Python version. Ignored if venv_path is provided and has a valid environment already.
41
+ python_path: Optional direct path to a Python executable to use (must include the executable). Takes precedence over python_version if both are provided.
42
+ venv_path: Optional path for the virtual environment, will create it if it doesn't exist. If None, creates a temp directory.
43
+ """
44
+ self.python_version = python_version
45
+ self.python_path = python_path
46
+ self.venv_path = venv_path
47
+ self.created_venv = False
48
+ self._executable = None
49
+ super().__init__()
50
+
51
+ def _setup_environment(self) -> None:
52
+ """Set up the virtual environment."""
53
+ # Create a venv directory if not provided
54
+ if self.venv_path is None:
55
+ self.venv_path = tempfile.mkdtemp(prefix="ag2_python_env_")
56
+ self.created_venv = True
57
+
58
+ # Determine the python version, getting it from the venv if it already has one
59
+ base_python = self._get_python_executable_for_version()
60
+ needs_creation = True
61
+ else:
62
+ # If venv_path is provided, check if it's already a valid venv
63
+ if os.name == "nt": # Windows
64
+ venv_python = os.path.join(self.venv_path, "Scripts", "python.exe")
65
+ else: # Unix-like (Mac/Linux)
66
+ venv_python = os.path.join(self.venv_path, "bin", "python")
67
+
68
+ if os.path.exists(venv_python) and os.access(venv_python, os.X_OK):
69
+ # Valid venv already exists, just use it
70
+ self._executable = venv_python
71
+ logging.info(f"Using existing virtual environment at {self.venv_path}")
72
+ needs_creation = False
73
+ else:
74
+ # Path exists but not a valid venv, or doesn't exist
75
+ if not os.path.exists(self.venv_path):
76
+ os.makedirs(self.venv_path, exist_ok=True)
77
+ self.created_venv = True
78
+ base_python = sys.executable
79
+ needs_creation = True
80
+
81
+ # Only create the venv if needed
82
+ if needs_creation:
83
+ logging.info(f"Creating virtual environment at {self.venv_path} using {base_python}")
84
+
85
+ try:
86
+ # Create the virtual environment
87
+ _ = subprocess.run(
88
+ [base_python, "-m", "venv", "--system-site-packages", self.venv_path],
89
+ check=True,
90
+ stdout=subprocess.PIPE,
91
+ stderr=subprocess.PIPE,
92
+ text=True,
93
+ )
94
+
95
+ # Determine the Python executable path
96
+ if os.name == "nt": # Windows
97
+ self._executable = os.path.join(self.venv_path, "Scripts", "python.exe")
98
+ else: # Unix-like (Mac/Linux)
99
+ self._executable = os.path.join(self.venv_path, "bin", "python")
100
+
101
+ # Verify the executable exists
102
+ if not os.path.exists(self._executable):
103
+ raise RuntimeError(
104
+ f"Virtual environment created but Python executable not found at {self._executable}"
105
+ )
106
+
107
+ except subprocess.CalledProcessError as e:
108
+ raise RuntimeError(f"Failed to create virtual environment: {e.stderr}") from e
109
+
110
+ def _cleanup_environment(self) -> None:
111
+ """Clean up the virtual environment."""
112
+ # Note: We intentionally don't clean up the venv here to allow
113
+ # tools to continue using it after the context exits.
114
+ pass
115
+
116
+ def get_executable(self) -> str:
117
+ """Get the path to the Python executable in the virtual environment."""
118
+ if not self._executable or not os.path.exists(self._executable):
119
+ raise RuntimeError("Virtual environment Python executable not found")
120
+ return self._executable
121
+
122
+ async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
123
+ """Execute code in the virtual environment."""
124
+ try:
125
+ # Get the Python executable
126
+ python_executable = self.get_executable()
127
+
128
+ # Verify the executable exists
129
+ if not os.path.exists(python_executable):
130
+ return {"success": False, "error": f"Python executable not found at {python_executable}"}
131
+
132
+ # Ensure the directory for the script exists
133
+ script_dir = os.path.dirname(script_path)
134
+ if script_dir:
135
+ os.makedirs(script_dir, exist_ok=True)
136
+
137
+ # Write the code to the script file using asyncify (from base class)
138
+ await asyncify(self._write_to_file)(script_path, code)
139
+
140
+ logging.info(f"Wrote code to {script_path}")
141
+
142
+ try:
143
+ # Execute directly with subprocess using asyncify for better reliability
144
+ result = await asyncify(self._run_subprocess)([python_executable, script_path], timeout)
145
+
146
+ # Main execution result
147
+ return {
148
+ "success": result.returncode == 0,
149
+ "stdout": result.stdout,
150
+ "stderr": result.stderr,
151
+ "returncode": result.returncode,
152
+ }
153
+ except subprocess.TimeoutExpired:
154
+ return {"success": False, "error": f"Execution timed out after {timeout} seconds"}
155
+
156
+ except Exception as e:
157
+ return {"success": False, "error": f"Execution error: {str(e)}"}
158
+
159
+ def _get_python_executable_for_version(self) -> str:
160
+ """Get the Python executable for the specified version and verify it can create a venv."""
161
+ # If a specific path is provided, use it directly
162
+ if self.python_path:
163
+ if not os.path.exists(self.python_path) or not os.access(self.python_path, os.X_OK):
164
+ raise RuntimeError(f"Python executable not found at {self.python_path}")
165
+ return self.python_path
166
+
167
+ # If no specific version is requested, use the current Python
168
+ if not self.python_version:
169
+ return sys.executable
170
+
171
+ potential_executables = []
172
+
173
+ # Try to find a specific Python version using pyenv if available
174
+ try:
175
+ pyenv_result = subprocess.run(
176
+ ["pyenv", "which", f"python{self.python_version}"],
177
+ check=True,
178
+ stdout=subprocess.PIPE,
179
+ stderr=subprocess.PIPE,
180
+ text=True,
181
+ )
182
+ potential_executables.append(pyenv_result.stdout.strip())
183
+ except (subprocess.SubprocessError, FileNotFoundError):
184
+ pass
185
+
186
+ # Try common system paths based on platform
187
+ if os.name == "nt": # Windows
188
+ potential_executables.extend([
189
+ f"C:\\Python{self.python_version.replace('.', '')}\\python.exe",
190
+ f"C:\\Program Files\\Python{self.python_version.replace('.', '')}\\python.exe",
191
+ f"C:\\Program Files (x86)\\Python{self.python_version.replace('.', '')}\\python.exe",
192
+ ])
193
+ else: # Unix-like (Mac and Linux)
194
+ # Add more paths that might exist on macOS
195
+ potential_executables.extend([
196
+ f"/usr/bin/python{self.python_version}",
197
+ f"/usr/local/bin/python{self.python_version}",
198
+ f"/opt/homebrew/bin/python{self.python_version}", # Homebrew on Apple Silicon
199
+ f"/opt/python/bin/python{self.python_version}",
200
+ ])
201
+
202
+ # Try each potential path and verify it can create a venv
203
+ for path in potential_executables:
204
+ if os.path.exists(path) and os.access(path, os.X_OK):
205
+ # Verify this Python can create a venv
206
+ try:
207
+ test_result = subprocess.run(
208
+ [path, "-m", "venv", "--help"],
209
+ check=False, # Don't raise exception
210
+ stdout=subprocess.PIPE,
211
+ stderr=subprocess.PIPE,
212
+ text=True,
213
+ timeout=5, # Add timeout for safety
214
+ )
215
+ if test_result.returncode == 0:
216
+ # Successfully found a valid Python executable
217
+ return path
218
+ except (subprocess.SubprocessError, FileNotFoundError):
219
+ continue
220
+
221
+ # If we couldn't find the specified version, raise an exception
222
+ raise RuntimeError(
223
+ f"Python {self.python_version} not found or cannot create virtual environments. Provide a python_path to use a specific Python executable."
224
+ )
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import contextlib
6
+ import os
7
+ import shutil
8
+ import tempfile
9
+ from contextvars import ContextVar
10
+ from typing import Optional
11
+
12
+ __all__ = ["WorkingDirectory"]
13
+
14
+
15
+ class WorkingDirectory:
16
+ """Context manager for changing the current working directory."""
17
+
18
+ _current_working_directory: ContextVar["WorkingDirectory"] = ContextVar("_current_working_directory")
19
+
20
+ def __init__(self, path: str):
21
+ """
22
+ Initialize with a directory path.
23
+
24
+ Args:
25
+ path: The directory path to change to.
26
+ """
27
+ self.path = path
28
+ self.original_path = None
29
+ self.created_tmp = False
30
+ self._token = None
31
+
32
+ def __enter__(self):
33
+ """Change to the specified directory and return self."""
34
+ self.original_path = os.getcwd()
35
+ if self.path:
36
+ os.makedirs(self.path, exist_ok=True)
37
+ os.chdir(self.path)
38
+
39
+ # Set this as the current working directory in the context
40
+ self._token = WorkingDirectory._current_working_directory.set(self)
41
+
42
+ return self
43
+
44
+ def __exit__(self, exc_type, exc_val, exc_tb):
45
+ """Change back to the original directory and clean up if necessary."""
46
+ # Reset the context variable if this was the active working directory
47
+ if self._token is not None:
48
+ WorkingDirectory._current_working_directory.reset(self._token)
49
+ self._token = None
50
+
51
+ if self.original_path:
52
+ os.chdir(self.original_path)
53
+ if self.created_tmp and self.path and os.path.exists(self.path):
54
+ with contextlib.suppress(Exception):
55
+ shutil.rmtree(self.path)
56
+
57
+ @classmethod
58
+ def create_tmp(cls):
59
+ """Create a temporary directory and return a WorkingDirectory instance for it."""
60
+ tmp_dir = tempfile.mkdtemp(prefix="ag2_work_dir_")
61
+ instance = cls(tmp_dir)
62
+ instance.created_tmp = True
63
+ return instance
64
+
65
+ @classmethod
66
+ def get_current_working_directory(
67
+ cls, working_directory: Optional["WorkingDirectory"] = None
68
+ ) -> Optional["WorkingDirectory"]:
69
+ """Get the current working directory or the specified one if provided."""
70
+ if working_directory is not None:
71
+ return working_directory
72
+ try:
73
+ return cls._current_working_directory.get()
74
+ except LookupError:
75
+ return None
autogen/llm_config.py CHANGED
@@ -9,7 +9,7 @@ from abc import ABC, abstractmethod
9
9
  from collections.abc import Iterable
10
10
  from contextvars import ContextVar
11
11
  from pathlib import Path
12
- from typing import TYPE_CHECKING, Annotated, Any, Mapping, Optional, Type, TypeVar, Union
12
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, Mapping, Optional, Type, TypeVar, Union
13
13
 
14
14
  from httpx import Client as httpxClient
15
15
  from pydantic import BaseModel, ConfigDict, Field, HttpUrl, SecretStr, ValidationInfo, field_serializer, field_validator
@@ -268,6 +268,7 @@ class LLMConfig(metaclass=MetaLLMConfig):
268
268
  list[Annotated[Union[llm_config_classes], Field(discriminator="api_type")]],
269
269
  Field(default_factory=list, min_length=1),
270
270
  ]
271
+ routing_method: Optional[Literal["fixed_order", "round_robin"]] = None
271
272
 
272
273
  # Following field is configuration for pydantic to disallow extra fields
273
274
  model_config = ConfigDict(extra="forbid")
@@ -302,13 +303,15 @@ class LLMConfigEntry(BaseModel, ABC):
302
303
  @field_validator("base_url", mode="before")
303
304
  @classmethod
304
305
  def check_base_url(cls, v: Any, info: ValidationInfo) -> Any:
306
+ if v is None: # Handle None case explicitly
307
+ return None
305
308
  if not str(v).startswith("https://") and not str(v).startswith("http://"):
306
309
  v = f"http://{str(v)}"
307
310
  return v
308
311
 
309
- @field_serializer("base_url")
312
+ @field_serializer("base_url", when_used="unless-none") # Ensure serializer also respects None
310
313
  def serialize_base_url(self, v: Any) -> Any:
311
- return str(v)
314
+ return str(v) if v is not None else None
312
315
 
313
316
  @field_serializer("api_key", when_used="unless-none")
314
317
  def serialize_api_key(self, v: SecretStr) -> Any:
@@ -293,7 +293,7 @@ class SqliteLogger(BaseLogger):
293
293
  client_id,
294
294
  wrapper_id,
295
295
  self.session_id,
296
- json.dumps(request),
296
+ json.dumps(to_dict(request)),
297
297
  response_messages,
298
298
  is_cached,
299
299
  cost,
@@ -129,12 +129,12 @@ class MCPProxy:
129
129
  return mcp
130
130
 
131
131
  def _process_params(
132
- self, path: str, func: Callable[[Any], Any], **kwargs: Any
132
+ self, process_path: str, func: Callable[[Any], Any], **kwargs: Any
133
133
  ) -> tuple[str, dict[str, Any], dict[str, Any]]:
134
- path = MCPProxy._convert_camel_case_within_braces_to_snake(path)
135
- q_params, path_params, body, security = MCPProxy._get_params(path, func)
134
+ process_path = MCPProxy._convert_camel_case_within_braces_to_snake(process_path)
135
+ q_params, path_params, body, security = MCPProxy._get_params(process_path, func)
136
136
 
137
- expanded_path = path.format(**{p: kwargs[p] for p in path_params})
137
+ expanded_path = process_path.format(**{p: kwargs[p] for p in path_params})
138
138
 
139
139
  url = self._servers[0]["url"] + expanded_path
140
140
 
autogen/oai/client.py CHANGED
@@ -246,6 +246,12 @@ class OpenAILLMConfigEntry(LLMConfigEntry):
246
246
  price: Optional[list[float]] = Field(default=None, min_length=2, max_length=2)
247
247
  tool_choice: Optional[Literal["none", "auto", "required"]] = None
248
248
  user: Optional[str] = None
249
+
250
+ # ⏺ The extra_body parameter flows from OpenAILLMConfigEntry to the LLM request through this path:
251
+ # 1. Config Definition: extra_body is defined in OpenAILLMConfigEntry (autogen/oai/client.py:248)
252
+ # 2. Parameter Classification: It's classified as an OpenAI client parameter (not AG2-specific) via the openai_kwargs property (autogen/oai/client.py:752-758)
253
+ # 3. Request Separation: In _separate_create_config() (autogen/oai/client.py:842), extra_body goes into create_config since it's not in the extra_kwargs set.
254
+ # 4. API Call: The create_config becomes params and gets passed directly to OpenAI's create() method via **params (autogen/oai/client.py:551,658)
249
255
  extra_body: Optional[dict[str, Any]] = (
250
256
  None # For VLLM - See here: https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html#extra-parameters
251
257
  )
@@ -806,17 +812,29 @@ class OpenAIWrapper:
806
812
  self._clients: list[ModelClient] = []
807
813
  self._config_list: list[dict[str, Any]] = []
808
814
 
815
+ # Determine routing_method from base_config only.
816
+ self.routing_method = base_config.get("routing_method") or "fixed_order"
817
+ self._round_robin_index = 0
818
+
819
+ # Remove routing_method from extra_kwargs after it has been used to set self.routing_method
820
+ # This ensures it's not part of the individual client configurations that are based on extra_kwargs.
821
+ extra_kwargs.pop("routing_method", None)
822
+
809
823
  if config_list:
810
824
  config_list = [config.copy() for config in config_list] # make a copy before modifying
811
- for config in config_list:
812
- self._register_default_client(config, openai_config) # could modify the config
813
- self._config_list.append({
814
- **extra_kwargs,
815
- **{k: v for k, v in config.items() if k not in self.openai_kwargs},
816
- })
825
+ for config_item in config_list:
826
+ self._register_default_client(config_item, openai_config)
827
+ # Construct current_config_extra_kwargs using the cleaned extra_kwargs
828
+ # (which doesn't have routing_method from base_config)
829
+ # and specific non-openai kwargs from config_item.
830
+ config_item_specific_extras = {k: v for k, v in config_item.items() if k not in self.openai_kwargs}
831
+ self._config_list.append({**extra_kwargs, **config_item_specific_extras})
817
832
  else:
833
+ # For a single config passed via base_config (already in extra_kwargs)
818
834
  self._register_default_client(extra_kwargs, openai_config)
835
+ # extra_kwargs has already had routing_method popped.
819
836
  self._config_list = [extra_kwargs]
837
+
820
838
  self.wrapper_id = id(self)
821
839
 
822
840
  def _separate_openai_config(self, config: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
@@ -1074,7 +1092,16 @@ class OpenAIWrapper:
1074
1092
  raise RuntimeError(
1075
1093
  f"Model client(s) {non_activated} are not activated. Please register the custom model clients using `register_model_client` or filter them out form the config list."
1076
1094
  )
1077
- for i, client in enumerate(self._clients):
1095
+
1096
+ ordered_clients_indices = list(range(len(self._clients)))
1097
+ if self.routing_method == "round_robin" and len(self._clients) > 0:
1098
+ ordered_clients_indices = (
1099
+ ordered_clients_indices[self._round_robin_index :] + ordered_clients_indices[: self._round_robin_index]
1100
+ )
1101
+ self._round_robin_index = (self._round_robin_index + 1) % len(self._clients)
1102
+
1103
+ for i in ordered_clients_indices:
1104
+ client = self._clients[i]
1078
1105
  # merge the input config with the i-th config in the config list
1079
1106
  full_config = {**config, **self._config_list[i]}
1080
1107
  # separate the config into create_config and extra_kwargs
autogen/oai/gemini.py CHANGED
@@ -126,6 +126,7 @@ class GeminiClient:
126
126
  PARAMS_MAPPING = {
127
127
  "max_tokens": "max_output_tokens",
128
128
  # "n": "candidate_count", # Gemini supports only `n=1`
129
+ "seed": "seed",
129
130
  "stop_sequences": "stop_sequences",
130
131
  "temperature": "temperature",
131
132
  "top_p": "top_p",