diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d34068d3abe..66d1189db58 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4277,7 +4277,7 @@ class ProxyConfig: Load and parse a YAML file """ try: - with open(file_path, "r") as file: + with open(file_path, "r", encoding="utf-8") as file: return yaml.safe_load(file) or {} except Exception as e: raise Exception(f"Error loading yaml file {file_path}: {e}") @@ -4298,7 +4298,7 @@ class ProxyConfig: # Load existing config ## Yaml if os.path.exists(f"{file_path}"): - with open(f"{file_path}", "r") as config_file: + with open(f"{file_path}", "r", encoding="utf-8") as config_file: config = yaml.safe_load(config_file) elif file_path is not None: raise Exception(f"Config file not found: {file_path}") diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index d1dada4d10e..f296208d334 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -8,6 +8,7 @@ Pins covered: from __future__ import annotations +import builtins import json import os import re @@ -465,6 +466,24 @@ def test_ProxyConfig__load_yaml_file_raises_on_missing_file(): pc._load_yaml_file("/no/such/file.yaml") +def _open_with_ascii_default(real_open): + def opener(file, mode="r", *args, encoding=None, **kwargs): + if "b" not in mode and encoding is None: + encoding = "ascii" + return real_open(file, mode, *args, encoding=encoding, **kwargs) + + return opener + + +def test_ProxyConfig__load_yaml_file_reads_utf8_under_non_utf8_locale(tmp_path, monkeypatch): + f = tmp_path / "c.yaml" + f.write_text('general_settings:\n alerting_args:\n daily_report_note: "Pākīhi – café"\n', encoding="utf-8") + monkeypatch.setattr(builtins, "open", _open_with_ascii_default(open)) + pc = ProxyConfig() + result = pc._load_yaml_file(str(f)) + assert result == {"general_settings": {"alerting_args": {"daily_report_note": "Pākīhi – café"}}} + + # --------------------------------------------------------------------------- # ProxyConfig._get_config_from_file # --------------------------------------------------------------------------- @@ -490,6 +509,19 @@ async def test_ProxyConfig__get_config_from_file_missing_path_raises(): await pc._get_config_from_file(config_file_path="/no/such/file.yaml") +@pytest.mark.asyncio +async def test_ProxyConfig__get_config_from_file_reads_utf8_under_non_utf8_locale(tmp_path, monkeypatch): + f = tmp_path / "c.yaml" + f.write_text( + 'model_list:\n - model_name: "summariser – bāsic"\n litellm_params:\n model: gpt-4o\n', + encoding="utf-8", + ) + monkeypatch.setattr(builtins, "open", _open_with_ascii_default(open)) + pc = ProxyConfig() + result = await pc._get_config_from_file(config_file_path=str(f)) + assert result == {"model_list": [{"model_name": "summariser – bāsic", "litellm_params": {"model": "gpt-4o"}}]} + + # --------------------------------------------------------------------------- # ProxyConfig._process_includes # ---------------------------------------------------------------------------