Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
38 changes: 38 additions & 0 deletions src/agents/models/openai_chatcompletions.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def __init__(
self._has_warned_unsupported_prompt = False
self._has_warned_unsupported_conversation_state = False
self._has_warned_unsupported_reasoning_settings = False
self._warned_unsupported_response_settings: set[str] = set()

def _non_null_or_omit(self, value: Any) -> Any:
return value if value is not None else omit
Expand Down Expand Up @@ -129,6 +130,42 @@ def _handle_unsupported_reasoning_settings(self, model_settings: ModelSettings)
)
self._has_warned_unsupported_reasoning_settings = True

def _handle_unsupported_response_settings(self, model_settings: ModelSettings) -> None:
unsupported = [
name
for name in ("truncation", "response_include", "context_management")
if getattr(model_settings, name, None) is not None
]
if not unsupported:
return

if self._strict_feature_validation:
raise UserError(self._unsupported_response_settings_message(unsupported))

# Warn per setting name rather than once overall. A later call that adds a
# setting not warned about yet would otherwise be dropped in silence, which is
# the behavior this handler exists to report.
unwarned = [
name for name in unsupported if name not in self._warned_unsupported_response_settings
]
if not unwarned:
return

logger.warning(
"%s Ignoring them; enable strict feature validation to raise an error instead.",
self._unsupported_response_settings_message(unwarned),
)
self._warned_unsupported_response_settings.update(unwarned)

@staticmethod
def _unsupported_response_settings_message(names: list[str]) -> str:
unsupported_params = ", ".join(f"`{name}`" for name in names)
return (
f"OpenAIChatCompletionsModel does not support {unsupported_params}. "
"These settings are only sent by the Responses API; use a Responses model "
"instead."
)

def get_retry_advice(self, request: ModelRetryAdviceRequest) -> ModelRetryAdvice | None:
return get_openai_retry_advice(request)

Expand Down Expand Up @@ -637,6 +674,7 @@ async def _fetch_response(
) -> ChatCompletion | tuple[Response, AsyncStream[ChatCompletionChunk]]:
self._handle_unsupported_prompt(prompt)
self._handle_unsupported_reasoning_settings(model_settings)
self._handle_unsupported_response_settings(model_settings)
self._validate_official_openai_input_content_types(input)
converted_messages = Converter.items_to_messages(
input,
Expand Down
79 changes: 79 additions & 0 deletions tests/models/test_openai_chatcompletions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,85 @@ def test_chat_completions_warns_once_for_responses_only_reasoning_settings(
assert "reasoning.context" in caplog.text


def test_chat_completions_warns_once_for_responses_only_response_settings(
caplog: pytest.LogCaptureFixture,
) -> None:
model = OpenAIChatCompletionsModel(
model="gpt-5.6-sol",
openai_client=cast(Any, object()),
)
model_settings = ModelSettings(
truncation="auto",
response_include=cast(Any, ["reasoning.encrypted_content"]),
context_management=cast(Any, [{"type": "transcript_trimming"}]),
)
caplog.set_level(logging.WARNING, logger="openai.agents")

model._handle_unsupported_response_settings(model_settings)
model._handle_unsupported_response_settings(model_settings)

assert caplog.text.count("Ignoring them") == 1
assert "`truncation`" in caplog.text
assert "`response_include`" in caplog.text
assert "`context_management`" in caplog.text


def test_chat_completions_warns_for_a_response_setting_added_on_a_later_call(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A setting introduced after the first warning must still be reported.

Suppressing it would drop the setting in silence, which is what this handler
exists to prevent.
"""
model = OpenAIChatCompletionsModel(
model="gpt-5.6-sol",
openai_client=cast(Any, object()),
)
caplog.set_level(logging.WARNING, logger="openai.agents")

model._handle_unsupported_response_settings(ModelSettings(truncation="auto"))
assert "`truncation`" in caplog.text
assert "`context_management`" not in caplog.text

caplog.clear()
model._handle_unsupported_response_settings(
ModelSettings(
truncation="auto",
context_management=cast(Any, [{"type": "transcript_trimming"}]),
)
)

# Only the newly introduced setting is named; truncation is not repeated.
assert "`context_management`" in caplog.text
assert "`truncation`" not in caplog.text

caplog.clear()
model._handle_unsupported_response_settings(ModelSettings(truncation="auto"))
assert caplog.text == ""


def test_chat_completions_rejects_responses_only_response_settings_in_strict_mode() -> None:
model = OpenAIChatCompletionsModel(
model="gpt-5.6-sol",
openai_client=cast(Any, object()),
strict_feature_validation=True,
)

with pytest.raises(UserError, match="`truncation`"):
model._handle_unsupported_response_settings(ModelSettings(truncation="auto"))


def test_chat_completions_allows_settings_the_responses_api_owns_when_unset() -> None:
model = OpenAIChatCompletionsModel(
model="gpt-5.6-sol",
openai_client=cast(Any, object()),
strict_feature_validation=True,
)

model._handle_unsupported_response_settings(ModelSettings())


def test_chat_completions_rejects_responses_only_reasoning_settings_in_strict_mode() -> None:
model = OpenAIChatCompletionsModel(
model="gpt-5.6-sol",
Expand Down