From 770b0da0b69737e430addf6d755c26b4f2af3cc4 Mon Sep 17 00:00:00 2001 From: Luke J Date: Wed, 22 Jul 2026 22:25:05 +1200 Subject: [PATCH] fix(proxy): read local config.yaml as utf-8 regardless of OS locale ProxyConfig loaded the local config file with the locale default encoding, which crashes or silently corrupts UTF-8 configs on Windows (cp1252). The S3 and GCS config loaders already decode UTF-8 explicitly; this brings the local path in line. Includes are covered via _load_yaml_file. --- litellm/proxy/proxy_server.py | 4 +-- .../proxy/proxy_server/test_proxy_config.py | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6de3e43fc1a..4ea33108d88 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3804,7 +3804,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}: {str(e)}") @@ -3825,7 +3825,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 8d1d8185e4d..1819e27bab8 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 from types import SimpleNamespace @@ -325,6 +326,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 # --------------------------------------------------------------------------- @@ -350,6 +369,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 # ---------------------------------------------------------------------------