diff --git a/python/packages/autogen-ext/src/autogen_ext/models/openai/config/__init__.py b/python/packages/autogen-ext/src/autogen_ext/models/openai/config/__init__.py index d0a17875875e..bdf3325841f8 100644 --- a/python/packages/autogen-ext/src/autogen_ext/models/openai/config/__init__.py +++ b/python/packages/autogen-ext/src/autogen_ext/models/openai/config/__init__.py @@ -1,4 +1,4 @@ -from typing import Awaitable, Callable, Dict, List, Literal, Optional, Union +from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Union from autogen_core import ComponentModel from autogen_core.models import ModelCapabilities, ModelInfo # type: ignore @@ -57,6 +57,10 @@ class CreateArguments(TypedDict, total=False): - 'low': Faster responses with less reasoning - 'medium': Balanced reasoning and speed - 'high': More thorough reasoning, may take longer""" + extra_body: Optional[Dict[str, Any]] + """Additional JSON properties to include in the request body, passed directly to the API. + Useful for provider-specific parameters not covered by the standard OpenAI interface + (e.g. ``{"enable_thinking": false}`` for some Qwen-compatible endpoints).""" AsyncAzureADTokenProvider = Callable[[], Union[str, Awaitable[str]]] @@ -108,6 +112,8 @@ class CreateArgumentsConfigModel(BaseModel): parallel_tool_calls: bool | None = None # Controls the amount of effort the model uses for reasoning (reasoning models only) reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None + # Additional JSON properties passed directly to the API request body + extra_body: Dict[str, Any] | None = None class BaseOpenAIClientConfigurationConfigModel(CreateArgumentsConfigModel): diff --git a/python/packages/autogen-ext/tests/models/test_openai_model_client.py b/python/packages/autogen-ext/tests/models/test_openai_model_client.py index ba79795d1ed7..2ac6a48b9545 100644 --- a/python/packages/autogen-ext/tests/models/test_openai_model_client.py +++ b/python/packages/autogen-ext/tests/models/test_openai_model_client.py @@ -3378,3 +3378,46 @@ async def test_reasoning_effort_validation() -> None: } ChatCompletionClient.load_component(config) + + +@pytest.mark.asyncio +async def test_extra_body_parameter() -> None: + """Test that extra_body is preserved through load_component round-trip (fixes #7418).""" + + from autogen_core.models import ChatCompletionClient + + # Test OpenAI client with extra_body via constructor + openai_client = OpenAIChatCompletionClient( + model="gpt-5", + api_key="fake_key", + extra_body={"enable_thinking": False}, + ) + assert openai_client._create_args["extra_body"] == {"enable_thinking": False} # pyright: ignore[reportPrivateUsage] + + # Test that extra_body survives load_component round-trip (was silently dropped before) + config = { + "provider": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-5", + "api_key": "fake_key", + "extra_body": {"enable_thinking": False}, + }, + } + loaded_client = ChatCompletionClient.load_component(config) + assert loaded_client._create_args["extra_body"] == {"enable_thinking": False} # type: ignore[attr-defined] # pyright: ignore[reportPrivateUsage, reportUnknownMemberType, reportAttributeAccessIssue] + + # Test Azure OpenAI client with extra_body via constructor + azure_client = AzureOpenAIChatCompletionClient( + model="gpt-5", + azure_endpoint="fake_endpoint", + azure_deployment="gpt-5-2025-08-07", + api_version="2025-02-01-preview", + api_key="fake_key", + extra_body={"reasoning": {"effort": "high"}}, + ) + assert azure_client._create_args["extra_body"] == {"reasoning": {"effort": "high"}} # pyright: ignore[reportPrivateUsage] + + # Test that extra_body survives dump/load cycle + dumped = openai_client.dump_component() + reloaded = OpenAIChatCompletionClient.load_component(dumped) + assert reloaded._create_args["extra_body"] == {"enable_thinking": False} # pyright: ignore[reportPrivateUsage]