fix(proxy): read config files as utf-8 instead of the locale encoding

Five call sites opened config files in text mode without an encoding, so
they decoded with locale.getpreferredencoding(). On a Windows host that
is cp1252 and a UTF-8 config fails two ways: model_name Café loads as
Café with no error raised, and any UTF-8 byte undefined in cp1252
(0x81, 0x8D, 0x8F, 0x90, 0x9D) raises UnicodeDecodeError. Cafe plus
U+0301 encodes to 43 61 66 65 CC 81 and is enough to trigger it.

Two of the five run at import time, so they are not limited to proxy
users, though endpoints.json is ASCII today which keeps that half
latent.

Three regression tests. The third runs the loader under
-X warn_default_encoding, which makes CPython raise on any open() that
omits an encoding, so it fails on Linux too rather than only on the
platform that has the bug. The filter is armed after the import so it
measures the config path and nothing else.
This commit is contained in:
Chirag 2026-08-17 15:42:08 +05:30
parent 973329e986
commit 19533c8265
5 changed files with 64 additions and 5 deletions

View file

@ -38,7 +38,7 @@ RESPONSE_TYPES: Final[dict[str, type]] = {
def _load_endpoints_config() -> dict:
"""Load the endpoints configuration from JSON file."""
config_path: Final = Path(__file__).parent / "endpoints.json"
with open(config_path) as f:
with open(config_path, encoding="utf-8") as f:
return json.load(f)

View file

@ -46,7 +46,7 @@ def load_snapshot() -> dict[str, dict] | None:
if not SNAPSHOT_FILE.exists():
return None
try:
with SNAPSHOT_FILE.open() as f:
with SNAPSHOT_FILE.open(encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None

View file

@ -28,7 +28,7 @@ from litellm.proxy.container_endpoints.ownership import (
def _load_endpoints_config() -> dict:
"""Load the endpoints configuration from JSON file."""
config_path: Final = Path(__file__).parent.parent.parent / "containers" / "endpoints.json"
with open(config_path) as f:
with open(config_path, encoding="utf-8") as f:
return json.load(f)

View file

@ -4096,7 +4096,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}")
@ -4117,7 +4117,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}")

View file

@ -11064,3 +11064,62 @@ async def test_ptu_rollup_job_not_registered_without_opt_in(monkeypatch):
assert scheduler.get_job(PTU_ROLLUP_JOB_ID) is None
assert len(scheduler.get_jobs()) > 0
CONFIG_WITH_NON_ASCII = """model_list:
- model_name: café-gpt
litellm_params:
model: openai/gpt-4o
api_key: sk-test
"""
def test_load_yaml_file_reads_non_ascii_config(tmp_path):
"""A UTF-8 config decoded with the locale encoding silently yields café-gpt on a cp1252 host,
so the wrong model name reaches routing with no error raised."""
from litellm.proxy.proxy_server import ProxyConfig
config_path = tmp_path / "config.yaml"
config_path.write_text(CONFIG_WITH_NON_ASCII, encoding="utf-8")
config = ProxyConfig()._load_yaml_file(str(config_path))
assert config["model_list"][0]["model_name"] == "café-gpt"
def test_load_yaml_file_reads_config_with_byte_undefined_in_cp1252(tmp_path):
"""Cafe plus U+0301 encodes to a 0x81 byte, which cp1252 has no mapping for, so this raised
UnicodeDecodeError rather than merely mangling the name."""
from litellm.proxy.proxy_server import ProxyConfig
config_path = tmp_path / "config.yaml"
config_path.write_text("model_list:\n - model_name: Café\n", encoding="utf-8")
config = ProxyConfig()._load_yaml_file(str(config_path))
assert config["model_list"][0]["model_name"] == "Café"
def test_load_yaml_file_specifies_an_encoding(tmp_path):
"""Asserting the decoded value passes on any UTF-8 host regardless of the fix, and CI is Linux.
Running the loader under warn_default_encoding fails on every platform instead."""
config_path = tmp_path / "config.yaml"
config_path.write_text(CONFIG_WITH_NON_ASCII, encoding="utf-8")
body = (
"import warnings\n"
"from litellm.proxy.proxy_server import ProxyConfig\n"
'warnings.simplefilter("error", EncodingWarning)\n'
f'config = ProxyConfig()._load_yaml_file(r"{config_path}")\n'
'assert config["model_list"][0]["model_name"] == "caf\u00e9-gpt", config\n'
'print("OK")\n'
)
proc = subprocess.run(
[sys.executable, "-X", "warn_default_encoding", "-W", "error::EncodingWarning", "-c", body],
capture_output=True,
text=True,
check=False,
)
assert "EncodingWarning" not in proc.stderr, proc.stderr
assert proc.returncode == 0, proc.stderr