import json from dataclasses import fields from openai.types.shared import Reasoning from pydantic import TypeAdapter from pydantic_core import to_json from agents.model_settings import MCPToolChoice, ModelSettings from agents.retry import ModelRetryBackoffSettings, ModelRetrySettings, retry_policies def verify_serialization(model_settings: ModelSettings) -> None: """Verify that ModelSettings can be serialized to a JSON string.""" json_dict = model_settings.to_json_dict() json_string = json.dumps(json_dict) assert json_string is not None def test_basic_serialization() -> None: """Tests whether ModelSettings can be serialized to a JSON string.""" # First, lets create a ModelSettings instance model_settings = ModelSettings( temperature=0.5, top_p=0.9, max_tokens=100, ) # Now, lets serialize the ModelSettings instance to a JSON string verify_serialization(model_settings) def test_mcp_tool_choice_serialization() -> None: """Tests whether ModelSettings with MCPToolChoice can be serialized to a JSON string.""" # First, lets create a ModelSettings instance model_settings = ModelSettings( temperature=0.5, tool_choice=MCPToolChoice(server_label="mcp", name="mcp_tool"), ) # Now, lets serialize the ModelSettings instance to a JSON string verify_serialization(model_settings) def test_all_fields_serialization() -> None: """Tests whether ModelSettings can be serialized to a JSON string.""" # First, lets create a ModelSettings instance model_settings = ModelSettings( temperature=0.5, top_p=0.9, frequency_penalty=0.0, presence_penalty=0.0, tool_choice="auto", parallel_tool_calls=True, truncation="auto", max_tokens=100, reasoning=Reasoning(), metadata={"foo": "bar"}, store=False, prompt_cache_retention="24h", include_usage=False, response_include=["reasoning.encrypted_content"], top_logprobs=1, verbosity="low", extra_query={"foo": "bar"}, extra_body={"foo": "bar"}, extra_headers={"foo": "bar"}, extra_args={"custom_param": "value", "another_param": 42}, retry=ModelRetrySettings( max_retries=2, backoff=ModelRetryBackoffSettings( initial_delay=0.1, max_delay=1.0, multiplier=2.0, jitter=False, ), ), context_management=[{"type": "compaction", "compact_threshold": 200000}], prompt_cache_options={"mode": "explicit", "ttl": "30m"}, ) # Verify that every single field is set to a non-None value for field in fields(model_settings): assert getattr(model_settings, field.name) is not None, ( f"You must set the {field.name} field" ) # Now, lets serialize the ModelSettings instance to a JSON string verify_serialization(model_settings) def test_gpt_5_6_reasoning_and_prompt_cache_serialization() -> None: model_settings = ModelSettings( reasoning=Reasoning(mode="pro", effort="max", context="all_turns"), prompt_cache_options={"mode": "explicit", "ttl": "30m"}, ) serialized_reasoning = model_settings.to_json_dict()["reasoning"] assert serialized_reasoning["context"] == "all_turns" assert serialized_reasoning["effort"] == "max" assert serialized_reasoning["mode"] == "pro" assert model_settings.to_traceable_dict()["prompt_cache_options"] == { "mode": "explicit", "ttl": "30m", } def test_prompt_cache_options_is_appended_to_public_field_order() -> None: field_names = [field.name for field in fields(ModelSettings)] assert field_names[-2:] == ["context_management", "prompt_cache_options"] def test_extra_args_serialization() -> None: """Test that extra_args are properly serialized.""" model_settings = ModelSettings( temperature=0.5, extra_args={"custom_param": "value", "another_param": 42, "nested": {"key": "value"}}, ) json_dict = model_settings.to_json_dict() assert json_dict["extra_args"] == { "custom_param": "value", "another_param": 42, "nested": {"key": "value"}, } # Verify serialization works verify_serialization(model_settings) def test_traceable_serialization_omits_request_extras() -> None: model_settings = ModelSettings( temperature=0.5, extra_headers={"Authorization": "Bearer provider-token"}, extra_query={"api-key": "query-token"}, extra_body={"secret": "body-token"}, extra_args={"api_key": "arg-token"}, ) json_dict = model_settings.to_json_dict() assert json_dict["extra_headers"] == {"Authorization": "Bearer provider-token"} assert json_dict["extra_query"] == {"api-key": "query-token"} assert json_dict["extra_body"] == {"secret": "body-token"} assert json_dict["extra_args"] == {"api_key": "arg-token"} traceable = model_settings.to_traceable_dict() assert traceable["temperature"] == 0.5 assert "extra_headers" not in traceable assert "extra_query" not in traceable assert "extra_body" not in traceable assert "extra_args" not in traceable def test_extra_args_resolve() -> None: """Test that extra_args are properly merged in the resolve method.""" base_settings = ModelSettings( temperature=0.5, extra_args={"param1": "base_value", "param2": "base_only"} ) override_settings = ModelSettings( top_p=0.9, extra_args={"param1": "override_value", "param3": "override_only"} ) resolved = base_settings.resolve(override_settings) # Check that regular fields are properly resolved assert resolved.temperature == 0.5 # from base assert resolved.top_p == 0.9 # from override # Check that extra_args are properly merged expected_extra_args = { "param1": "override_value", # override wins "param2": "base_only", # from base "param3": "override_only", # from override } assert resolved.extra_args == expected_extra_args def test_extra_args_resolve_with_none() -> None: """Test that resolve works properly when one side has None extra_args.""" # Base with extra_args, override with None base_settings = ModelSettings(extra_args={"param1": "value1"}) override_settings = ModelSettings(temperature=0.8) resolved = base_settings.resolve(override_settings) assert resolved.extra_args == {"param1": "value1"} assert resolved.temperature == 0.8 # Base with None, override with extra_args base_settings = ModelSettings(temperature=0.5) override_settings = ModelSettings(extra_args={"param2": "value2"}) resolved = base_settings.resolve(override_settings) assert resolved.extra_args == {"param2": "value2"} assert resolved.temperature == 0.5 def test_extra_args_resolve_both_none() -> None: """Test that resolve works when both sides have None extra_args.""" base_settings = ModelSettings(temperature=0.5) override_settings = ModelSettings(top_p=0.9) resolved = base_settings.resolve(override_settings) assert resolved.extra_args is None assert resolved.temperature == 0.5 assert resolved.top_p == 0.9 def test_pydantic_serialization() -> None: """Tests whether ModelSettings can be serialized with Pydantic.""" # First, lets create a ModelSettings instance model_settings = ModelSettings( temperature=0.5, top_p=0.9, frequency_penalty=0.0, presence_penalty=0.0, tool_choice="auto", parallel_tool_calls=True, truncation="auto", max_tokens=100, reasoning=Reasoning(), metadata={"foo": "bar"}, store=False, include_usage=False, top_logprobs=1, extra_query={"foo": "bar"}, extra_body={"foo": "bar"}, extra_headers={"foo": "bar"}, extra_args={"custom_param": "value", "another_param": 42}, ) json = to_json(model_settings) deserialized = TypeAdapter(ModelSettings).validate_json(json) assert model_settings == deserialized def test_retry_policy_is_excluded_from_json_dict() -> None: """Tests whether runtime-only retry policies are omitted from JSON serialization.""" model_settings = ModelSettings( retry=ModelRetrySettings( max_retries=1, backoff=ModelRetryBackoffSettings(initial_delay=0.1), policy=retry_policies.http_status([429]), ) ) json_dict = model_settings.to_json_dict() assert json_dict["retry"] == { "max_retries": 1, "backoff": { "initial_delay": 0.1, "max_delay": None, "multiplier": None, "jitter": None, }, } verify_serialization(model_settings) def test_retry_resolve_deep_merges_backoff() -> None: """Tests whether retry settings are deep-merged in resolve().""" base_settings = ModelSettings( retry=ModelRetrySettings( max_retries=1, backoff=ModelRetryBackoffSettings(initial_delay=0.1, max_delay=1.0), ) ) override_settings = ModelSettings( retry=ModelRetrySettings( backoff=ModelRetryBackoffSettings(multiplier=3.0, jitter=False), policy=retry_policies.never(), ) ) resolved = base_settings.resolve(override_settings) assert resolved.retry is not None assert resolved.retry.max_retries == 1 assert resolved.retry.policy is not None assert resolved.retry.backoff == ModelRetryBackoffSettings( initial_delay=0.1, max_delay=1.0, multiplier=3.0, jitter=False, ) def test_retry_policy_is_omitted_from_pydantic_round_trip() -> None: """Tests whether runtime-only retry policies are omitted from Pydantic serialization.""" model_settings = ModelSettings( retry=ModelRetrySettings( max_retries=2, backoff=ModelRetryBackoffSettings(initial_delay=0.5), policy=retry_policies.http_status([429]), ) ) serialized = to_json(model_settings) deserialized = TypeAdapter(ModelSettings).validate_json(serialized) assert deserialized.retry is not None assert deserialized.retry.max_retries == 2 assert deserialized.retry.backoff == ModelRetryBackoffSettings(initial_delay=0.5) assert deserialized.retry.policy is None def test_retry_backoff_validate_python_accepts_nested_dict_input() -> None: """Tests whether nested retry/backoff dict input is coerced to dataclasses.""" deserialized = TypeAdapter(ModelSettings).validate_python( { "retry": { "max_retries": 3, "backoff": { "initial_delay": 0.25, "max_delay": 2.0, "multiplier": 3.0, "jitter": False, }, } } ) assert deserialized.retry is not None assert deserialized.retry.max_retries == 3 assert deserialized.retry.backoff == ModelRetryBackoffSettings( initial_delay=0.25, max_delay=2.0, multiplier=3.0, jitter=False, ) def test_retry_backoff_validate_python_preserves_falsey_values() -> None: """Tests whether falsey-only retry backoff input survives validation and serialization.""" deserialized = TypeAdapter(ModelRetrySettings).validate_python( { "max_retries": 1, "backoff": { "jitter": False, }, } ) assert deserialized.backoff == ModelRetryBackoffSettings(jitter=False) assert deserialized.to_json_dict()["backoff"] == { "initial_delay": None, "max_delay": None, "multiplier": None, "jitter": False, }