mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
fix(router): keep the TypeSafe key off caller-chosen Jev endpoints
This commit is contained in:
parent
0a66328663
commit
7f581f6bc7
4 changed files with 68 additions and 0 deletions
|
|
@ -65,6 +65,21 @@ class _MemberRouterGenerationParams(BaseModel):
|
|||
stop: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
class _MemberJevClassifierConfig(BaseModel):
|
||||
"""The Jev classifier settings a team member may set. Credentials stay the proxy's own: a member-chosen
|
||||
api_base would receive the proxy's TYPESAFE_API_KEY, and a member-chosen api_key would be sent from the proxy."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
model: str
|
||||
api_key: None = None
|
||||
api_base: None = None
|
||||
timeout_ms: int
|
||||
instructions: str | None = None
|
||||
circuit_breaker_enabled: bool
|
||||
circuit_breaker_cooldown_seconds: float
|
||||
|
||||
|
||||
class _MemberComplexityRouterConfig(RequestComplexityRouterConfig):
|
||||
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
||||
|
||||
|
|
@ -113,6 +128,8 @@ def validate_member_auto_router_config(config: Mapping[str, object]) -> RequestC
|
|||
for entries in validated.tier_model_configs.values():
|
||||
for entry in entries:
|
||||
_MemberRouterGenerationParams.model_validate(entry.litellm_params)
|
||||
if validated.jev_classifier_config is not None:
|
||||
_MemberJevClassifierConfig.model_validate(validated.jev_classifier_config.model_dump())
|
||||
return validated
|
||||
except ValidationError as exc:
|
||||
location: Final = ".".join(str(part) for part in exc.errors()[0]["loc"])
|
||||
|
|
|
|||
|
|
@ -697,6 +697,15 @@ class JevClassifierConfig(BaseModel):
|
|||
raise ValueError("jev_classifier_config.instructions must be non-empty; omit it to use the default")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _keep_the_environment_key_on_the_environment_base(self) -> "JevClassifierConfig":
|
||||
if self.api_base is not None and self.api_key is None:
|
||||
raise ValueError(
|
||||
"jev_classifier_config.api_base requires jev_classifier_config.api_key: TYPESAFE_API_KEY is only sent "
|
||||
"to TYPESAFE_API_BASE or https://api.typesafe.ai"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
MAX_CUSTOM_PATTERN_REPEAT: Final[int] = 64
|
||||
MAX_CUSTOM_PATTERN_WORK: Final[int] = 2048
|
||||
|
|
|
|||
|
|
@ -131,6 +131,38 @@ def test_tier_config_is_normalized_and_unknown_router_extras_are_rejected() -> N
|
|||
validate_member_auto_router_config({"tiers": {"SIMPLE": "allowed"}, "api_base": "https://example.invalid"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("jev_override", "rejected_at"),
|
||||
[
|
||||
({"api_base": "https://collector.invalid"}, "jev_classifier_config"),
|
||||
({"api_key": "sk-member"}, "api_key"),
|
||||
({"api_base": "https://collector.invalid", "api_key": "sk-member"}, "api_key"),
|
||||
],
|
||||
)
|
||||
def test_members_cannot_move_the_jev_classifier_off_the_proxys_typesafe_account(
|
||||
jev_override: Mapping[str, str], rejected_at: str
|
||||
) -> None:
|
||||
with pytest.raises(HTTPException) as denied:
|
||||
validate_member_auto_router_config(
|
||||
{"tiers": {"SIMPLE": "allowed"}, "classifier_type": "jev", "jev_classifier_config": jev_override}
|
||||
)
|
||||
assert denied.value.status_code == 400
|
||||
assert denied.value.detail == f"Invalid member auto-router configuration at {rejected_at}."
|
||||
|
||||
|
||||
def test_members_can_still_tune_the_jev_classifier() -> None:
|
||||
validated: Final = validate_member_auto_router_config(
|
||||
{
|
||||
"tiers": {"SIMPLE": "allowed"},
|
||||
"classifier_type": "jev",
|
||||
"jev_classifier_config": {"model": "jev-preview", "timeout_ms": 500},
|
||||
}
|
||||
)
|
||||
assert validated.jev_classifier_config is not None
|
||||
assert (validated.jev_classifier_config.model, validated.jev_classifier_config.timeout_ms) == ("jev-preview", 500)
|
||||
assert validate_member_auto_router_config(validated.model_dump()).jev_classifier_config is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"patch_fields",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,16 @@ def test_jev_instructions_reject_blank_values() -> None:
|
|||
JevClassifierConfig(instructions=" \t")
|
||||
|
||||
|
||||
def test_jev_api_base_without_its_own_key_is_rejected_so_the_environment_key_stays_home() -> None:
|
||||
with pytest.raises(ValueError, match=r"api_base requires jev_classifier_config\.api_key"):
|
||||
ComplexityRouterConfig.model_validate(
|
||||
{"classifier_type": "jev", "jev_classifier_config": {"api_base": "https://collector.invalid"}}
|
||||
)
|
||||
paired: Final = JevClassifierConfig(api_base="https://eu.typesafe.invalid", api_key="sk-own")
|
||||
assert (paired.api_base, paired.api_key) == ("https://eu.typesafe.invalid", "sk-own")
|
||||
assert JevClassifierConfig(api_key="sk-own").api_base is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("probabilities", "confidence"),
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue