Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ag2/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
ContainerInfo,
ContainerManager,
ExpiresAfter,
OpenAICompletionsConfig,
OpenAIConfig,
OpenAIDecisionsConfig,
OpenAIResponsesConfig,
)
except ImportError as e:
OpenAICompletionsConfig = missing_optional_dependency("OpenAICompletionsConfig", "openai", e) # type: ignore[misc]
OpenAIConfig = missing_optional_dependency("OpenAIConfig", "openai", e) # type: ignore[misc]
OpenAIResponsesConfig = missing_optional_dependency("OpenAIResponsesConfig", "openai", e) # type: ignore[misc]
OpenAIDecisionsConfig = missing_optional_dependency("OpenAIDecisionsConfig", "openai", e) # type: ignore[misc]
Expand Down Expand Up @@ -83,6 +85,7 @@
"ModelConfig",
"ModelProvider",
"OllamaConfig",
"OpenAICompletionsConfig",
"OpenAIConfig",
"OpenAIDecisionsConfig",
"OpenAIResponsesConfig",
Expand Down
7 changes: 5 additions & 2 deletions ag2/config/input_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MistralConfig,
ModelConfig,
OllamaConfig,
OpenAICompletionsConfig,
OpenAIConfig,
OpenAIResponsesConfig,
TypeSafeConfig,
Expand All @@ -29,7 +30,7 @@
# Each entry names (position, source, kind). Text is accepted everywhere.
# The table describes mapper behavior, not model-specific ability.
_MEDIA: dict[type[object], set[tuple[str, str, BinaryType | None]]] = {
OpenAIConfig: {
OpenAICompletionsConfig: {
("user", "data", BinaryType.IMAGE),
("user", "url", BinaryType.IMAGE),
("user", "data", BinaryType.AUDIO),
Expand Down Expand Up @@ -110,6 +111,8 @@ def _described(config: ModelConfig | None) -> type[object] | None:
return None
# Compared by identity, never `isinstance`: a provider whose extra is not
# installed is a stand-in here, not a class.
if type(config) is OpenAIConfig:
return OpenAICompletionsConfig
return type(config) if type(config) in _MEDIA else None


Expand Down Expand Up @@ -162,7 +165,7 @@ def accepts_input(config: ModelConfig | None, position: str, part: Input) -> boo
},
}
return kind is not None and part.media_type in supported_media.get(kind, set())
if described is OpenAIConfig and kind is BinaryType.AUDIO:
if described is OpenAICompletionsConfig and kind is BinaryType.AUDIO:
return part.media_type in ("audio/wav", "audio/mpeg", "audio/mp3")
if described is AnthropicConfig and kind is BinaryType.IMAGE:
return part.media_type in ("image/jpeg", "image/png", "image/gif", "image/webp")
Expand Down
3 changes: 2 additions & 1 deletion ag2/config/openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0

from .config import OpenAIConfig, OpenAIDecisionsConfig, OpenAIResponsesConfig
from .config import OpenAICompletionsConfig, OpenAIConfig, OpenAIDecisionsConfig, OpenAIResponsesConfig
from .containers import ContainerInfo, ContainerManager, ExpiresAfter
from .decisions_mappers import DecisionRefusedError, UnsupportedResponseSchemaError
from .openai_client import OpenAIClient
Expand All @@ -15,6 +15,7 @@
"DecisionRefusedError",
"ExpiresAfter",
"OpenAIClient",
"OpenAICompletionsConfig",
"OpenAIConfig",
"OpenAIDecisionsClient",
"OpenAIDecisionsConfig",
Expand Down
26 changes: 23 additions & 3 deletions ag2/config/openai/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from openai.types.responses import ServiceTier as ResponseServiceTier
from openai.types.responses.response_create_params import PromptCacheOptions as ResponsePromptCacheOptions
from openai.types.shared_params import DecisionQuestionInputPart, Reasoning
from typing_extensions import Unpack
from typing_extensions import Self, Unpack, deprecated

from ag2.config.config import ModelConfig, ModelProvider
from ag2.events import Input
Expand Down Expand Up @@ -75,7 +75,9 @@ class OpenAIConfigOverrides(TypedDict, total=False):


@dataclass(slots=True)
class OpenAIConfig(ModelConfig):
class OpenAICompletionsConfig(ModelConfig):
"""Configure the OpenAI Chat Completions API, including compatible endpoints."""

model: ChatModel | str
api_key: str | None = None
base_url: str | None = None
Expand Down Expand Up @@ -127,7 +129,7 @@ class OpenAIConfig(ModelConfig):
def provider(self) -> ModelProvider:
return ModelProvider.OPENAI

def copy(self, /, **overrides: Unpack[OpenAIConfigOverrides]) -> "OpenAIConfig":
def copy(self, /, **overrides: Unpack[OpenAIConfigOverrides]) -> Self:
return replace(self, **overrides)

def create(self) -> OpenAIClient:
Expand Down Expand Up @@ -183,6 +185,24 @@ def create_files_client(self) -> OpenAIFilesClient:
return OpenAIFilesClient(self)


OPENAI_CONFIG_DEPRECATION = (
"OpenAIConfig as a Chat Completions config is deprecated and will be removed in 1.4.0. "
"Use OpenAICompletionsConfig explicitly to keep Chat Completions, or OpenAIResponsesConfig for the Responses API. "
"In 1.4.0, OpenAIConfig will become an alias for OpenAIResponsesConfig."
)


@deprecated(OPENAI_CONFIG_DEPRECATION, category=None)
class OpenAIConfig(OpenAICompletionsConfig):
"""Compatibility name for Chat Completions until the Responses alias switch in 1.4.0."""

__slots__ = ()

@deprecated(OPENAI_CONFIG_DEPRECATION)
def create(self) -> OpenAIClient:
return super().create()


class OpenAIResponsesConfigOverrides(TypedDict, total=False):
model: ChatModel | str
api_key: str | None
Expand Down
4 changes: 2 additions & 2 deletions ag2/config/openai/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
from ag2.files.types import FileContent, FileProvider, UploadedFile, _created_at_to_float

if TYPE_CHECKING:
from ag2.config.openai.config import OpenAIConfig, OpenAIResponsesConfig
from ag2.config.openai.config import OpenAICompletionsConfig, OpenAIResponsesConfig


class OpenAIFilesClient:
"""Files API client for OpenAI."""

__slots__ = ("_client",)

def __init__(self, config: "OpenAIConfig | OpenAIResponsesConfig") -> None:
def __init__(self, config: "OpenAICompletionsConfig | OpenAIResponsesConfig") -> None:
self._client = AsyncOpenAI(
api_key=config.api_key,
organization=config.organization,
Expand Down
2 changes: 1 addition & 1 deletion ag2/live/cascade.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class CascadeConfig(RealtimeConfig):
"assistant",
config=CascadeConfig(
stt=ElevenLabsTranscriber("scribe_v2"),
model=config.OpenAIConfig("gpt-5-mini", streaming=True),
model=config.OpenAIResponsesConfig("gpt-5-mini", streaming=True),
tts=ElevenLabsStreamingTTSConfig("eleven_flash_v2_5"),
),
)
Expand Down
2 changes: 1 addition & 1 deletion ag2/tools/builtin/image_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ImageGenerationTool(Tool):

Instructs the model to generate images inline during the conversation.
Only supported with ``OpenAIResponsesConfig`` — raises ``UnsupportedToolError``
when used with ``OpenAIConfig`` (Chat Completions API).
when used with ``OpenAICompletionsConfig`` (Chat Completions API).

Generated images are returned as ``list[BinaryResult]`` via ``reply.files``.

Expand Down
32 changes: 32 additions & 0 deletions docs/adr/0026-openai-config-transitions-to-responses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
status: accepted
date: 2026-10-27
---

# 0026. OpenAIConfig transitions to the Responses API in 1.4.0

## Context

`OpenAIConfig` currently selects Chat Completions. Responses is the intended
OpenAI default, but immediately rebinding the name would change endpoints and
reject Chat Completions parameters in existing applications.

## Decision

Expose the existing implementation as `OpenAICompletionsConfig`. Until 1.4.0,
`OpenAIConfig` remains a compatibility subclass with the same constructor,
copy behavior and client. Mark the class deprecated for type checkers and emit
a `DeprecationWarning` from `create()`, following ADR 0025's rule that
construction and copying stay free of side effects.

In 1.4.0, remove the deprecated Chat Completions binding and bind
`OpenAIConfig` to `OpenAIResponsesConfig`. Recommend the explicit classes
during the transition: `OpenAICompletionsConfig` preserves Chat Completions;
`OpenAIResponsesConfig` opts into Responses now.

## Consequences

Existing code keeps its endpoint during the deprecation period. The explicit
Chat Completions class remains supported after 1.4.0, including for compatible
endpoints. Applications migrating to Responses must translate API-specific
options; changing the name alone does not translate those options.
4 changes: 2 additions & 2 deletions test/config/openai/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pytest
from dirty_equals import IsFloat

from ag2.config import OpenAIConfig
from ag2.config import OpenAICompletionsConfig
from ag2.config.openai.files import OpenAIFilesClient
from ag2.files import FilesAPI
from ag2.files.types import FileContent, FileProvider, UploadedFile
Expand Down Expand Up @@ -48,7 +48,7 @@ def handler(request: httpx2.Request) -> httpx2.Response:
)

async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http_client:
files = await FilesAPI(OpenAIConfig(model="test", api_key="test", http_client=http_client)).list()
files = await FilesAPI(OpenAICompletionsConfig(model="test", api_key="test", http_client=http_client)).list()

assert files == [
UploadedFile(
Expand Down
12 changes: 8 additions & 4 deletions test/config/openai/test_http_client_family.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from fast_depends.use import SerializerCls

from ag2 import Context, MemoryStream
from ag2.config import OpenAIConfig, OpenAIResponsesConfig
from ag2.config import OpenAICompletionsConfig, OpenAIResponsesConfig
from ag2.events import ModelRequest, TextInput

_COMPLETION = {
Expand Down Expand Up @@ -74,7 +74,7 @@ def handler(request: httpx.Request) -> httpx.Response:
return httpx.AsyncClient(transport=httpx.MockTransport(handler))


async def _ask(config: OpenAIConfig | OpenAIResponsesConfig) -> None:
async def _ask(config: OpenAICompletionsConfig | OpenAIResponsesConfig) -> None:
client = config.create()
await client(
messages=[ModelRequest([TextInput("capital of France?")])],
Expand All @@ -99,7 +99,9 @@ class TestHttpx2Client:
async def test_completions(self) -> None:
captured: dict[str, object] = {}

await _ask(OpenAIConfig(model="gpt-4o", api_key="test", http_client=_modern_client(captured, _COMPLETION)))
await _ask(
OpenAICompletionsConfig(model="gpt-4o", api_key="test", http_client=_modern_client(captured, _COMPLETION))
)

assert captured["body"] == _COMPLETION_BODY

Expand All @@ -120,7 +122,9 @@ class TestLegacyHttpxClient:
async def test_completions(self) -> None:
captured: dict[str, object] = {}

await _ask(OpenAIConfig(model="gpt-4o", api_key="test", http_client=_legacy_client(captured, _COMPLETION)))
await _ask(
OpenAICompletionsConfig(model="gpt-4o", api_key="test", http_client=_legacy_client(captured, _COMPLETION))
)

assert captured["body"] == _COMPLETION_BODY

Expand Down
14 changes: 8 additions & 6 deletions test/config/openai/test_openai_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
#
# SPDX-License-Identifier: Apache-2.0

from ag2.config import OpenAIConfig
from ag2.config import OpenAICompletionsConfig


def test_copy_without_overrides_returns_new_equal_instance() -> None:
config = OpenAIConfig(model="gpt-5", temperature=0.2, streaming=True)
config = OpenAICompletionsConfig(model="gpt-5", temperature=0.2, streaming=True)

copied = config.copy()

Expand All @@ -15,7 +15,7 @@ def test_copy_without_overrides_returns_new_equal_instance() -> None:


def test_copy_applies_overrides_without_mutating_original() -> None:
config = OpenAIConfig(model="gpt-5", api_key="key", temperature=0.2, streaming=False)
config = OpenAICompletionsConfig(model="gpt-5", api_key="key", temperature=0.2, streaming=False)

copied = config.copy(model="gpt-5-mini", temperature=0.8, streaming=True, api_key=None)

Expand All @@ -31,22 +31,24 @@ def test_copy_applies_overrides_without_mutating_original() -> None:


def test_extra_body_defaults_to_none() -> None:
config = OpenAIConfig(model="gpt-5")
config = OpenAICompletionsConfig(model="gpt-5")

assert config.extra_body is None


def test_extra_body_passed_to_create_options() -> None:
extra = {"chat_template_kwargs": {"enable_thinking": True}}
config = OpenAIConfig(model="test-model", base_url="http://localhost:8000/v1", api_key="EMPTY", extra_body=extra)
config = OpenAICompletionsConfig(
model="test-model", base_url="http://localhost:8000/v1", api_key="EMPTY", extra_body=extra
)

client = config.create()

assert client._create_options.get("extra_body") == extra


def test_copy_with_extra_body() -> None:
config = OpenAIConfig(model="gpt-5")
config = OpenAICompletionsConfig(model="gpt-5")
extra = {"reasoning_split": True}

copied = config.copy(extra_body=extra)
Expand Down
Loading
Loading