fix(proxy): return actionable 500 on invalid Relay settings schema and stop leaking file path

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-07-14 04:58:59 +00:00
parent 926727fa4c
commit 3d44fdcf73
3 changed files with 30 additions and 13 deletions

View file

@ -3,6 +3,7 @@ from pathlib import Path
import yaml
from fastapi import APIRouter, Depends, HTTPException
from pydantic import ValidationError
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.types.proxy.relay_endpoints import RelayManagedConfigResponse
@ -26,7 +27,7 @@ def _load_managed_config(path: Path) -> RelayManagedConfigResponse:
except yaml.YAMLError as e:
raise HTTPException(
status_code=500,
detail={"error": f"Failed to parse Relay settings at {path}: {e}"},
detail={"error": f"Failed to parse Relay settings ({RELAY_SETTINGS_PATH_ENV}): {e}"},
)
if parsed is None:
@ -34,9 +35,17 @@ def _load_managed_config(path: Path) -> RelayManagedConfigResponse:
if not isinstance(parsed, dict):
raise HTTPException(
status_code=500,
detail={"error": f"Relay settings at {path} must be a YAML mapping, got {type(parsed).__name__}"},
detail={
"error": f"Relay settings ({RELAY_SETTINGS_PATH_ENV}) must be a YAML mapping, got {type(parsed).__name__}"
},
)
try:
return RelayManagedConfigResponse.model_validate(parsed)
except ValidationError as e:
raise HTTPException(
status_code=500,
detail={"error": f"Relay settings ({RELAY_SETTINGS_PATH_ENV}) failed schema validation: {e}"},
)
return RelayManagedConfigResponse.model_validate(parsed)
@router.get(

View file

@ -1,28 +1,26 @@
from typing import Dict, Optional
from pydantic import BaseModel, Field, JsonValue
class RelayClaudeCodeConfig(BaseModel):
channel: str = Field(default="pinned")
version: Optional[str] = Field(default=None)
version: str | None = Field(default=None)
registry: str = Field(default="npm")
package: str = Field(default="@anthropic-ai/claude-code")
model: Optional[str] = Field(default=None)
managed_settings: Dict[str, JsonValue] = Field(default_factory=dict)
model: str | None = Field(default=None)
managed_settings: dict[str, JsonValue] = Field(default_factory=dict)
class RelayCodexConfig(BaseModel):
channel: str = Field(default="pinned")
version: Optional[str] = Field(default=None)
version: str | None = Field(default=None)
registry: str = Field(default="npm")
package: str = Field(default="@openai/codex")
model: Optional[str] = Field(default=None)
model: str | None = Field(default=None)
class RelayManagedConfigResponse(BaseModel):
claude_code: RelayClaudeCodeConfig = Field(default_factory=RelayClaudeCodeConfig)
codex: RelayCodexConfig = Field(default_factory=RelayCodexConfig)
policy_version: Optional[int] = Field(default=None)
updated_by: Optional[str] = Field(default=None)
updated_at: Optional[str] = Field(default=None)
policy_version: int | None = Field(default=None)
updated_by: str | None = Field(default=None)
updated_at: str | None = Field(default=None)

View file

@ -109,6 +109,16 @@ def test_load_managed_config_rejects_invalid_yaml(tmp_path: Path) -> None:
assert exc_info.value.status_code == 500
def test_load_managed_config_rejects_wrong_field_type(tmp_path: Path) -> None:
path = tmp_path / "relay_settings.yaml"
path.write_text("policy_version: not-an-integer\n")
with pytest.raises(HTTPException) as exc_info:
_load_managed_config(path)
assert exc_info.value.status_code == 500
def test_load_managed_config_preserves_nested_managed_settings(tmp_path: Path) -> None:
path = tmp_path / "relay_settings.yaml"
path.write_text(