Skip to content

Commit 574a598

Browse files
authored
feat: add context management model setting (#3128)
1 parent 7a5d32b commit 574a598

6 files changed

Lines changed: 118 additions & 3 deletions

File tree

src/agents/model_settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from openai import Omit as _Omit
88
from openai._types import Body, Query
99
from openai.types.responses import ResponseIncludable
10+
from openai.types.responses.response_create_params import ContextManagement
1011
from openai.types.shared import Reasoning
1112
from pydantic import GetCoreSchemaHandler, TypeAdapter
1213
from pydantic.dataclasses import dataclass
@@ -162,6 +163,13 @@ class ModelSettings:
162163
retry: ModelRetrySettings | None = None
163164
"""Opt-in runner-managed retry settings for model calls."""
164165

166+
context_management: list[ContextManagement] | None = None
167+
"""Context management entries for OpenAI Responses API requests.
168+
169+
For example, use ``[{"type": "compaction", "compact_threshold": 200000}]``
170+
to enable server-side compaction when the rendered context crosses a token threshold.
171+
"""
172+
165173
def resolve(self, override: ModelSettings | None) -> ModelSettings:
166174
"""Produce a new ModelSettings by overlaying any non-None values from the
167175
override on top of this instance."""

src/agents/models/openai_responses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,7 @@ def _build_response_create_kwargs(
852852
"prompt_cache_retention": self._non_null_or_omit(model_settings.prompt_cache_retention),
853853
"reasoning": self._non_null_or_omit(model_settings.reasoning),
854854
"metadata": self._non_null_or_omit(model_settings.metadata),
855+
"context_management": self._non_null_or_omit(model_settings.context_management),
855856
}
856857
duplicate_extra_arg_keys = sorted(set(create_kwargs).intersection(extra_args))
857858
if duplicate_extra_arg_keys:

tests/model_settings/test_serialization.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def test_all_fields_serialization() -> None:
7575
jitter=False,
7676
),
7777
),
78+
context_management=[{"type": "compaction", "compact_threshold": 200000}],
7879
)
7980

8081
# Verify that every single field is set to a non-None value

tests/models/test_openai_responses.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010
from openai import NOT_GIVEN, APIConnectionError, RateLimitError, omit
1111
from openai.types.responses import ResponseCompletedEvent, ResponseErrorEvent
12+
from openai.types.responses.response_create_params import ContextManagement
1213
from openai.types.shared.reasoning import Reasoning
1314

1415
from agents import (
@@ -843,6 +844,53 @@ def test_build_response_create_kwargs_includes_extra_args_prompt_cache_key():
843844
assert kwargs["prompt_cache_key"] == "cache-key"
844845

845846

847+
@pytest.mark.allow_call_model_methods
848+
def test_build_response_create_kwargs_includes_context_management():
849+
client = DummyWSClient()
850+
model = OpenAIResponsesModel(model="gpt-4", openai_client=client) # type: ignore[arg-type]
851+
context_management: list[ContextManagement] = [
852+
{"type": "compaction", "compact_threshold": 200000}
853+
]
854+
855+
kwargs = model._build_response_create_kwargs(
856+
system_instructions=None,
857+
input="hi",
858+
model_settings=ModelSettings(context_management=context_management),
859+
tools=[],
860+
output_schema=None,
861+
handoffs=[],
862+
previous_response_id=None,
863+
conversation_id=None,
864+
stream=False,
865+
prompt=None,
866+
)
867+
868+
assert kwargs["context_management"] == context_management
869+
870+
871+
@pytest.mark.allow_call_model_methods
872+
def test_build_response_create_kwargs_rejects_duplicate_context_management_extra_args():
873+
client = DummyWSClient()
874+
model = OpenAIResponsesModel(model="gpt-4", openai_client=client) # type: ignore[arg-type]
875+
876+
with pytest.raises(TypeError, match="multiple values.*context_management"):
877+
model._build_response_create_kwargs(
878+
system_instructions=None,
879+
input="hi",
880+
model_settings=ModelSettings(
881+
context_management=[{"type": "compaction", "compact_threshold": 200000}],
882+
extra_args={"context_management": [{"type": "compaction"}]},
883+
),
884+
tools=[],
885+
output_schema=None,
886+
handoffs=[],
887+
previous_response_id=None,
888+
conversation_id=None,
889+
stream=False,
890+
prompt=None,
891+
)
892+
893+
846894
@pytest.mark.allow_call_model_methods
847895
@pytest.mark.asyncio
848896
async def test_custom_base_url_prompt_cache_key_uses_model_settings_only() -> None:

tests/test_prompt_cache_key.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import pytest
4+
from openai.types.responses.response_create_params import ContextManagement
45

56
from agents import Agent, ModelSettings, RunConfig, Runner
67

@@ -125,7 +126,7 @@ async def test_runner_respects_existing_extra_body_prompt_cache_key() -> None:
125126
async def test_runner_generates_prompt_cache_key_with_unrelated_extra_args() -> None:
126127
model = PromptCacheFakeModel()
127128
model.set_next_output([get_text_message("done")])
128-
model_settings = ModelSettings(extra_args={"context_management": [{"type": "compaction"}]})
129+
model_settings = ModelSettings(extra_args={"service_tier": "flex"})
129130
agent = Agent(
130131
name="test",
131132
model=model,
@@ -137,10 +138,34 @@ async def test_runner_generates_prompt_cache_key_with_unrelated_extra_args() ->
137138
assert _sent_prompt_cache_key(model) is not None
138139
sent_model_settings = _sent_model_settings(model)
139140
assert sent_model_settings.extra_args == {
140-
"context_management": [{"type": "compaction"}],
141+
"service_tier": "flex",
141142
"prompt_cache_key": _sent_prompt_cache_key(model),
142143
}
143-
assert model_settings.extra_args == {"context_management": [{"type": "compaction"}]}
144+
assert model_settings.extra_args == {"service_tier": "flex"}
145+
146+
147+
@pytest.mark.asyncio
148+
async def test_runner_preserves_context_management_when_adding_prompt_cache_key() -> None:
149+
model = PromptCacheFakeModel()
150+
model.set_next_output([get_text_message("done")])
151+
context_management: list[ContextManagement] = [
152+
{"type": "compaction", "compact_threshold": 200000}
153+
]
154+
model_settings = ModelSettings(context_management=context_management)
155+
agent = Agent(
156+
name="test",
157+
model=model,
158+
model_settings=model_settings,
159+
)
160+
161+
await Runner.run(agent, "hi")
162+
163+
assert _sent_prompt_cache_key(model) is not None
164+
sent_model_settings = _sent_model_settings(model)
165+
assert sent_model_settings.context_management == context_management
166+
assert sent_model_settings.extra_args == {"prompt_cache_key": _sent_prompt_cache_key(model)}
167+
assert model_settings.context_management == context_management
168+
assert model_settings.extra_args is None
144169

145170

146171
@pytest.mark.asyncio

tests/test_source_compat_constructors.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
FunctionTool,
1010
HandoffInputData,
1111
ItemHelpers,
12+
ModelRetrySettings,
13+
ModelSettings,
1214
MultiProvider,
1315
RunConfig,
1416
RunContextWrapper,
@@ -92,6 +94,36 @@ def test_run_config_reasoning_item_id_policy_positional_binding() -> None:
9294
assert config.reasoning_item_id_policy == "omit"
9395

9496

97+
def test_model_settings_context_management_append_preserves_retry_position() -> None:
98+
retry = ModelRetrySettings(max_retries=1)
99+
settings = ModelSettings(
100+
None,
101+
None,
102+
None,
103+
None,
104+
None,
105+
None,
106+
None,
107+
None,
108+
None,
109+
None,
110+
None,
111+
None,
112+
None,
113+
None,
114+
None,
115+
None,
116+
None,
117+
None,
118+
None,
119+
None,
120+
retry,
121+
)
122+
123+
assert settings.retry is retry
124+
assert settings.context_management is None
125+
126+
95127
def test_function_tool_positional_arguments_keep_guardrail_positions() -> None:
96128
async def invoke(_ctx: ToolContext[Any], _args: str) -> str:
97129
return "ok"

0 commit comments

Comments
 (0)