|
| 1 | +""" |
| 2 | +Tests for ConfigGeneralSettings type coercion and dict compatibility. |
| 3 | +
|
| 4 | +This test ensures that the ConfigGeneralSettings Pydantic model properly |
| 5 | +coerces types from YAML/dict input, providing consistent behavior with |
| 6 | +environment variable type coercion. |
| 7 | +""" |
| 8 | + |
| 9 | +import pytest |
| 10 | +from litellm.proxy._types import ConfigGeneralSettings |
| 11 | + |
| 12 | + |
| 13 | +def test_config_general_settings_integer_type_coercion(): |
| 14 | + """Test that integer fields are properly coerced from string values.""" |
| 15 | + config_dict = { |
| 16 | + "proxy_batch_write_at": "10", # String should be coerced to int |
| 17 | + "proxy_batch_polling_interval": "6000", |
| 18 | + "proxy_budget_rescheduler_min_time": "597", |
| 19 | + "proxy_budget_rescheduler_max_time": "605", |
| 20 | + "health_check_interval": "300", |
| 21 | + "alerting_threshold": "600", |
| 22 | + "max_parallel_requests": "100", |
| 23 | + "maximum_spend_logs_retention_period": "30", |
| 24 | + } |
| 25 | + |
| 26 | + settings = ConfigGeneralSettings(**config_dict) |
| 27 | + |
| 28 | + # Verify integers are properly coerced |
| 29 | + assert settings.proxy_batch_write_at == 10 |
| 30 | + assert isinstance(settings.proxy_batch_write_at, int) |
| 31 | + assert settings.proxy_batch_polling_interval == 6000 |
| 32 | + assert isinstance(settings.proxy_batch_polling_interval, int) |
| 33 | + assert settings.proxy_budget_rescheduler_min_time == 597 |
| 34 | + assert isinstance(settings.proxy_budget_rescheduler_min_time, int) |
| 35 | + assert settings.health_check_interval == 300 |
| 36 | + assert isinstance(settings.health_check_interval, int) |
| 37 | + assert settings.alerting_threshold == 600 |
| 38 | + assert settings.max_parallel_requests == 100 |
| 39 | + assert settings.maximum_spend_logs_retention_period == 30 |
| 40 | + |
| 41 | + |
| 42 | +def test_config_general_settings_boolean_type_coercion(): |
| 43 | + """Test that boolean fields are properly coerced from string values.""" |
| 44 | + config_dict = { |
| 45 | + "disable_spend_logs": "true", |
| 46 | + "store_model_in_db": "false", |
| 47 | + "use_shared_health_check": "True", |
| 48 | + "disable_reset_budget": "False", |
| 49 | + "health_check_details": "yes", # Pydantic accepts yes/no |
| 50 | + "infer_model_from_keys": "1", # Pydantic accepts 1/0 |
| 51 | + } |
| 52 | + |
| 53 | + settings = ConfigGeneralSettings(**config_dict) |
| 54 | + |
| 55 | + # Verify booleans are properly coerced |
| 56 | + assert settings.disable_spend_logs is True |
| 57 | + assert isinstance(settings.disable_spend_logs, bool) |
| 58 | + assert settings.store_model_in_db is False |
| 59 | + assert isinstance(settings.store_model_in_db, bool) |
| 60 | + assert settings.use_shared_health_check is True |
| 61 | + assert settings.disable_reset_budget is False |
| 62 | + assert settings.health_check_details is True |
| 63 | + assert settings.infer_model_from_keys is True |
| 64 | + |
| 65 | + |
| 66 | +def test_config_general_settings_float_type_coercion(): |
| 67 | + """Test that float fields are properly coerced from string values.""" |
| 68 | + config_dict = { |
| 69 | + "user_api_key_cache_ttl": "60.5", |
| 70 | + "database_connection_timeout": "120", |
| 71 | + } |
| 72 | + |
| 73 | + settings = ConfigGeneralSettings(**config_dict) |
| 74 | + |
| 75 | + assert settings.user_api_key_cache_ttl == 60.5 |
| 76 | + assert isinstance(settings.user_api_key_cache_ttl, float) |
| 77 | + assert settings.database_connection_timeout == 120.0 |
| 78 | + assert isinstance(settings.database_connection_timeout, float) |
| 79 | + |
| 80 | + |
| 81 | +def test_config_general_settings_get_method_with_none(): |
| 82 | + """Test that .get() method returns default when value is None.""" |
| 83 | + settings = ConfigGeneralSettings() |
| 84 | + |
| 85 | + # When field is None, .get() should return the default |
| 86 | + assert settings.get("proxy_batch_write_at", 10) == 10 |
| 87 | + assert settings.get("master_key", "default_key") == "default_key" |
| 88 | + assert settings.get("disable_spend_logs", True) is True |
| 89 | + assert settings.get("moderation_model", "gpt-4") == "gpt-4" |
| 90 | + |
| 91 | + |
| 92 | +def test_config_general_settings_get_method_with_values(): |
| 93 | + """Test that .get() method returns actual value when set.""" |
| 94 | + settings = ConfigGeneralSettings( |
| 95 | + proxy_batch_write_at=15, |
| 96 | + master_key="test_key", |
| 97 | + disable_spend_logs=False, |
| 98 | + ) |
| 99 | + |
| 100 | + assert settings.get("proxy_batch_write_at", 10) == 15 |
| 101 | + assert settings.get("master_key", "default_key") == "test_key" |
| 102 | + assert settings.get("disable_spend_logs", True) is False |
| 103 | + |
| 104 | + |
| 105 | +def test_config_general_settings_extra_fields_allowed(): |
| 106 | + """Test that extra fields (not defined in model) are allowed and preserved.""" |
| 107 | + config_dict = { |
| 108 | + "proxy_batch_write_at": 10, |
| 109 | + "custom_field_not_in_model": "some_value", |
| 110 | + "another_custom_field": 123, |
| 111 | + } |
| 112 | + |
| 113 | + settings = ConfigGeneralSettings(**config_dict) |
| 114 | + |
| 115 | + # Defined field should work |
| 116 | + assert settings.proxy_batch_write_at == 10 |
| 117 | + |
| 118 | + # Extra fields should be accessible |
| 119 | + assert settings.get("custom_field_not_in_model") == "some_value" |
| 120 | + assert settings.get("another_custom_field") == 123 |
| 121 | + |
| 122 | + |
| 123 | +def test_config_general_settings_dict_style_access(): |
| 124 | + """Test dict-style access methods work correctly.""" |
| 125 | + settings = ConfigGeneralSettings(proxy_batch_write_at=20, master_key="test") |
| 126 | + |
| 127 | + # __getitem__ |
| 128 | + assert settings["proxy_batch_write_at"] == 20 |
| 129 | + assert settings["master_key"] == "test" |
| 130 | + |
| 131 | + # __contains__ |
| 132 | + assert "proxy_batch_write_at" in settings |
| 133 | + assert "master_key" in settings |
| 134 | + assert "nonexistent_field" not in settings |
| 135 | + |
| 136 | + # __setitem__ |
| 137 | + settings["new_field"] = "new_value" |
| 138 | + assert settings.get("new_field") == "new_value" |
| 139 | + |
| 140 | + # items() |
| 141 | + items = dict(settings.items()) |
| 142 | + assert "proxy_batch_write_at" in items |
| 143 | + assert items["proxy_batch_write_at"] == 20 |
| 144 | + |
| 145 | + |
| 146 | +def test_config_general_settings_update_method(): |
| 147 | + """Test that .update() method works like dict.update().""" |
| 148 | + settings = ConfigGeneralSettings(proxy_batch_write_at=10) |
| 149 | + |
| 150 | + # Update with dict |
| 151 | + settings.update({"proxy_batch_write_at": 20, "master_key": "new_key"}) |
| 152 | + |
| 153 | + assert settings.proxy_batch_write_at == 20 |
| 154 | + assert settings.master_key == "new_key" |
| 155 | + |
| 156 | + # Update should raise TypeError for non-dict |
| 157 | + with pytest.raises(TypeError): |
| 158 | + settings.update("not a dict") |
| 159 | + |
| 160 | + |
| 161 | +def test_config_general_settings_end_to_end_yaml_simulation(): |
| 162 | + """ |
| 163 | + End-to-end test simulating loading from YAML config. |
| 164 | + This is the key test that validates the original issue is fixed. |
| 165 | + """ |
| 166 | + # Simulate YAML config being loaded as dict (YAML parsers return strings) |
| 167 | + yaml_config = { |
| 168 | + "general_settings": { |
| 169 | + "proxy_batch_write_at": "10", # String from YAML |
| 170 | + "master_key": "sk-1234", |
| 171 | + "disable_spend_logs": "false", # String from YAML |
| 172 | + "user_api_key_cache_ttl": "60.5", # String from YAML |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + # Instantiate ConfigGeneralSettings from the dict |
| 177 | + general_settings = ConfigGeneralSettings(**yaml_config["general_settings"]) |
| 178 | + |
| 179 | + # Verify type coercion happened correctly |
| 180 | + assert general_settings.proxy_batch_write_at == 10 |
| 181 | + assert isinstance(general_settings.proxy_batch_write_at, int) |
| 182 | + |
| 183 | + # This is the critical part - can be used in arithmetic without errors |
| 184 | + # (Original issue was that strings from YAML weren't coerced) |
| 185 | + batch_interval = general_settings.proxy_batch_write_at + 5 |
| 186 | + assert batch_interval == 15 |
| 187 | + |
| 188 | + # Verify other coercions |
| 189 | + assert general_settings.disable_spend_logs is False |
| 190 | + assert isinstance(general_settings.disable_spend_logs, bool) |
| 191 | + assert general_settings.user_api_key_cache_ttl == 60.5 |
| 192 | + assert isinstance(general_settings.user_api_key_cache_ttl, float) |
| 193 | + |
| 194 | + # Verify .get() works with fallback to defaults |
| 195 | + assert general_settings.get("proxy_batch_write_at", 999) == 10 |
| 196 | + assert general_settings.get("nonexistent", "default") == "default" |
0 commit comments