mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-24 05:45:11 +00:00
fix(router): preserve fallback aliases and check synchronous text calls
Some checks failed
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
Some checks failed
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
This commit is contained in:
parent
7e527c39e7
commit
2ba59fb460
4 changed files with 105 additions and 7 deletions
|
|
@ -2519,6 +2519,17 @@ class ProxyBaseLLMRequestProcessing:
|
|||
original_model,
|
||||
fallback_models,
|
||||
)
|
||||
raw_router_settings: Final = _DECODED_JSON.validate_python(
|
||||
_REQUEST_FIELDS.validate_python(rate_limited_data).get("router_settings_override")
|
||||
)
|
||||
group_alias: Final = _DECODED_JSON.validate_python(
|
||||
_REQUEST_FIELDS.validate_python(raw_router_settings).get("model_group_alias")
|
||||
if isinstance(raw_router_settings, Mapping)
|
||||
else None
|
||||
)
|
||||
key_aliases: Final[object] = cast( # cast-ok: auth aliases are dynamically typed at runtime
|
||||
object, user_api_key_dict.aliases
|
||||
)
|
||||
try:
|
||||
for fallback_model in fallback_models:
|
||||
if fallback_model == original_model:
|
||||
|
|
@ -2534,8 +2545,17 @@ class ProxyBaseLLMRequestProcessing:
|
|||
self.data = independent_snapshot(pristine)
|
||||
self.data["model"] = fallback_model
|
||||
try:
|
||||
probe_data = {
|
||||
**_REQUEST_FIELDS.validate_python(pristine),
|
||||
"model": resolve_served_model(
|
||||
fallback_model,
|
||||
_DECODED_JSON.validate_python(litellm.model_alias_map),
|
||||
key_aliases,
|
||||
group_alias,
|
||||
),
|
||||
}
|
||||
if (
|
||||
endpoint_unsupported_by_model(route_type, self.data, llm_router, user_api_key_dict.team_id)
|
||||
endpoint_unsupported_by_model(route_type, probe_data, llm_router, user_api_key_dict.team_id)
|
||||
is not None
|
||||
):
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -5534,12 +5534,13 @@ class Router:
|
|||
kwargs.setdefault("metadata", {}).update({"model_group": model})
|
||||
|
||||
# pick the one that is available (lowest TPM/RPM)
|
||||
deployment: Final = self.get_available_deployment(
|
||||
model=model,
|
||||
messages=messages,
|
||||
specific_deployment=kwargs.pop("specific_deployment", None),
|
||||
request_kwargs=kwargs,
|
||||
)
|
||||
with endpoint_selection("text_completion", model=model):
|
||||
deployment: Final = self.get_available_deployment(
|
||||
model=model,
|
||||
messages=messages,
|
||||
specific_deployment=kwargs.pop("specific_deployment", None),
|
||||
request_kwargs=kwargs,
|
||||
)
|
||||
|
||||
data: Final = deployment["litellm_params"].copy()
|
||||
for k, v in self.default_litellm_params.items():
|
||||
|
|
|
|||
|
|
@ -1439,3 +1439,47 @@ def test_a_compaction_endpoint_declaration_keeps_the_model_available_for_compact
|
|||
_served_by_every_worker(rig.gateway, frozenset(identities))
|
||||
response: Final = rig.gateway.request("POST", "/responses/compact", {"model": model, "input": marker})
|
||||
_assert_served(rig, response, marker, "/v1/responses/compact")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alias_source", ["key_alias", "key_router", "team_router"])
|
||||
def test_local_rate_limit_fallbacks_resolve_saved_aliases(rig: _Rig, tmp_path: Path, alias_source: str) -> None:
|
||||
suffix: Final = uuid.uuid4().hex[:12]
|
||||
names: Final = {label: f"gate-fallback-alias-{label}-{suffix}" for label in ("primary", "target", "alias")}
|
||||
shared: Final = _CONFIG.validate_python(yaml.safe_load((_TESTS_ROOT / "integration/proxy_config.yaml").read_text()))
|
||||
config: Final = {
|
||||
**shared,
|
||||
"model_list": [
|
||||
_routed_entry(names["primary"], rig.api_base, f"openai/{_CHAT_MODEL}", {"mode": "chat"}),
|
||||
_routed_entry(names["target"], rig.api_base, f"openai/{_CHAT_MODEL}", {"mode": "chat"}),
|
||||
],
|
||||
"router_settings": {
|
||||
**object_value(shared["router_settings"]),
|
||||
"num_retries": 0,
|
||||
"fallbacks": [{names["primary"]: [names["alias"]]}],
|
||||
},
|
||||
}
|
||||
path: Final = tmp_path / "fallback-alias.yaml"
|
||||
path.write_text(yaml.safe_dump(config, sort_keys=False))
|
||||
aliases: Final = {names["alias"]: names["target"]}
|
||||
with (
|
||||
owned_proxy_process(rig.gateway, tmp_path, {}, config=path, workers=2) as owned,
|
||||
owned.gateway.scenario() as scenario,
|
||||
):
|
||||
team: Final = (
|
||||
scenario.team(router_settings={"model_group_alias": aliases}) if alias_source == "team_router" else None
|
||||
)
|
||||
key: Final = scenario.key(
|
||||
team_id=team,
|
||||
aliases=aliases if alias_source == "key_alias" else {},
|
||||
router_settings={"model_group_alias": aliases} if alias_source == "key_router" else None,
|
||||
model_rpm_limit={names["primary"]: 1},
|
||||
)
|
||||
first: Final = owned.gateway.request(
|
||||
"POST", "/v1/chat/completions", _chat_body(names["primary"], _marker()), key=key
|
||||
)
|
||||
assert first.status_code == 200, first.text
|
||||
marker: Final = _marker()
|
||||
second: Final = owned.gateway.request(
|
||||
"POST", "/v1/chat/completions", _chat_body(names["primary"], marker), key=key
|
||||
)
|
||||
_assert_fallback_answered(rig.upstream, second, marker)
|
||||
|
|
|
|||
|
|
@ -11285,6 +11285,39 @@ def test_is_deployment_blocked_static_helper_reflects_blocked_flag():
|
|||
)
|
||||
|
||||
|
||||
def test_synchronous_text_completion_rejects_a_restricted_model_before_the_provider() -> None:
|
||||
model: Final = "text-completion-openai/unit-text-model"
|
||||
router: Final = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": model,
|
||||
"litellm_params": {
|
||||
"model": model,
|
||||
"api_base": "https://sync-router.invalid/v1",
|
||||
"api_key": "unit-provider-key",
|
||||
},
|
||||
"model_info": {"enforce_supported_endpoints": True, "supported_endpoints": ["/v1/embeddings"]},
|
||||
}
|
||||
],
|
||||
num_retries=0,
|
||||
)
|
||||
with respx.mock(assert_all_called=False) as upstream:
|
||||
provider: Final = upstream.post("https://sync-router.invalid/v1/completions").respond(
|
||||
200,
|
||||
json={
|
||||
"id": "cmpl-unit",
|
||||
"object": "text_completion",
|
||||
"created": 1,
|
||||
"model": "unit-text-model",
|
||||
"choices": [{"text": "provider called", "index": 0, "finish_reason": "stop", "logprobs": None}],
|
||||
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
},
|
||||
)
|
||||
with pytest.raises(litellm.BadRequestError, match="/v1/completions is not supported"):
|
||||
router.text_completion(model=model, prompt="test")
|
||||
assert provider.call_count == 0
|
||||
|
||||
|
||||
def test_endpoint_preflight_returns_unavailable_model_errors_as_values() -> None:
|
||||
router: Final = Router(
|
||||
model_list=[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue