mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(router): keep a lone deployment warm under router allowed_fails unless the policy names the exception (#48644)
* fix(router): keep a lone deployment warm under router allowed_fails unless the policy names the exception Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(router): give cooldown fixtures a sibling deployment so allowed_fails=0 still forces a cooldown * fix(router): keep benching a lone deployment that has a fallback group or an empty deployment policy * fix(router): accept dictionary fallback targets in the lone deployment check Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
a53ea5bd77
commit
8f31c7171d
5 changed files with 234 additions and 31 deletions
|
|
@ -185,7 +185,14 @@ def _should_cooldown_based_on_deployment_policy(
|
||||||
override that safety net, matching `_has_explicit_allowed_fails_policy_for_exception`.
|
override that safety net, matching `_has_explicit_allowed_fails_policy_for_exception`.
|
||||||
"""
|
"""
|
||||||
allowed_fails_from_policy: Final = _resolve_allowed_fails_from_policy(dep_policy, original_exception)
|
allowed_fails_from_policy: Final = _resolve_allowed_fails_from_policy(dep_policy, original_exception)
|
||||||
if allowed_fails_from_policy is None and dep_allowed_fails is not None and is_single_deployment_model_group:
|
if (
|
||||||
|
allowed_fails_from_policy is None
|
||||||
|
and is_single_deployment_model_group
|
||||||
|
and (
|
||||||
|
dep_allowed_fails is not None
|
||||||
|
or litellm_router_instance.get_allowed_fails_from_policy(original_exception) is None
|
||||||
|
)
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
allowed_fails_override: Final[int | None] = (
|
allowed_fails_override: Final[int | None] = (
|
||||||
|
|
@ -399,6 +406,34 @@ def _should_run_cooldown_logic(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_FALLBACK_ENTRIES_ADAPTER: Final = TypeAdapter(tuple[Mapping[str, list[str | Mapping[str, object]]] | str, ...])
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_target_name(target: str | Mapping[str, object]) -> str:
|
||||||
|
return target if isinstance(target, str) else str(target.get("model", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_chain_names(
|
||||||
|
entry: Mapping[str, Sequence[str | Mapping[str, object]]],
|
||||||
|
) -> Mapping[str, list[str]]: # mutable-ok: mirrors get_fallback_model_group's fallbacks shape
|
||||||
|
return {key: [_fallback_target_name(target) for target in targets] for key, targets in entry.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _has_fallback_group(litellm_router_instance: LitellmRouter, requested_model_group: str | None) -> bool:
|
||||||
|
from litellm.router_utils.fallback_event_handlers import get_fallback_model_group
|
||||||
|
|
||||||
|
configured: Final[object] = getattr(litellm_router_instance, "fallbacks", None)
|
||||||
|
if requested_model_group is None or not configured:
|
||||||
|
return False
|
||||||
|
chains: Final = tuple(
|
||||||
|
_fallback_chain_names(entry)
|
||||||
|
for entry in _FALLBACK_ENTRIES_ADAPTER.validate_python(configured)
|
||||||
|
if isinstance(entry, Mapping)
|
||||||
|
)
|
||||||
|
fallback_group, _ = get_fallback_model_group(fallbacks=chains, model_group=requested_model_group)
|
||||||
|
return bool(fallback_group)
|
||||||
|
|
||||||
|
|
||||||
def _should_cooldown_deployment(
|
def _should_cooldown_deployment(
|
||||||
litellm_router_instance: LitellmRouter,
|
litellm_router_instance: LitellmRouter,
|
||||||
deployment: str,
|
deployment: str,
|
||||||
|
|
@ -431,6 +466,9 @@ def _should_cooldown_deployment(
|
||||||
litellm_router_instance.routing_group_has_alternatives(requested_model_group)
|
litellm_router_instance.routing_group_has_alternatives(requested_model_group)
|
||||||
or litellm_router_instance.team_model_has_alternatives(deployment)
|
or litellm_router_instance.team_model_has_alternatives(deployment)
|
||||||
)
|
)
|
||||||
|
is_lone_deployment: Final = is_single_deployment_model_group and not _has_fallback_group(
|
||||||
|
litellm_router_instance, requested_model_group
|
||||||
|
)
|
||||||
|
|
||||||
exception_status_int: Final = cast_exception_status_to_int(exception_status)
|
exception_status_int: Final = cast_exception_status_to_int(exception_status)
|
||||||
dep_policy, dep_allowed_fails = _get_deployment_cooldown_policy(litellm_router_instance, deployment)
|
dep_policy, dep_allowed_fails = _get_deployment_cooldown_policy(litellm_router_instance, deployment)
|
||||||
|
|
@ -450,7 +488,7 @@ def _should_cooldown_deployment(
|
||||||
original_exception,
|
original_exception,
|
||||||
dep_policy,
|
dep_policy,
|
||||||
dep_allowed_fails,
|
dep_allowed_fails,
|
||||||
is_single_deployment_model_group,
|
is_lone_deployment,
|
||||||
)
|
)
|
||||||
|
|
||||||
## BASE CASE - single deployment
|
## BASE CASE - single deployment
|
||||||
|
|
@ -495,6 +533,8 @@ def _should_cooldown_deployment(
|
||||||
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
if is_lone_deployment and litellm_router_instance.get_allowed_fails_from_policy(original_exception) is None:
|
||||||
|
return False
|
||||||
return should_cooldown_based_on_allowed_fails_policy(
|
return should_cooldown_based_on_allowed_fails_policy(
|
||||||
litellm_router_instance=litellm_router_instance,
|
litellm_router_instance=litellm_router_instance,
|
||||||
deployment=deployment,
|
deployment=deployment,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -390,6 +391,79 @@ def test_a_router_allowed_fails_policy_cools_a_lone_402_deployment_past_its_coun
|
||||||
assert markers_received(refusing) == (first.marker, second.marker)
|
assert markers_received(refusing) == (first.marker, second.marker)
|
||||||
|
|
||||||
|
|
||||||
|
def failing_reply(request: Request) -> Reply:
|
||||||
|
return model_list_reply() if is_model_info_probe(request) else anthropic_error(500, "upstream down")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_failed(outcome: Outcome) -> None:
|
||||||
|
assert outcome.status == 500, (outcome.status, outcome.detail)
|
||||||
|
assert "upstream down" in outcome.detail, outcome.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(900)
|
||||||
|
def test_a_router_allowed_fails_count_leaves_a_lone_deployment_warm_but_benches_one_with_a_sibling_or_fallback(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
lone_group: Final = new_group()
|
||||||
|
pair_group: Final = new_group()
|
||||||
|
primary_group: Final = new_group()
|
||||||
|
backup_group: Final = new_group()
|
||||||
|
with ExitStack() as stack:
|
||||||
|
upstream: Final = stack.enter_context(gateway_from_environment())
|
||||||
|
cache: Final = stack.enter_context(owned_redis(tmp_path))
|
||||||
|
settings: Final = router_settings(cache, allowed_fails=1, fallbacks=[{primary_group: [backup_group]}])
|
||||||
|
owned: Final = stack.enter_context(
|
||||||
|
owned_proxy_process(
|
||||||
|
upstream,
|
||||||
|
tmp_path,
|
||||||
|
{},
|
||||||
|
config=cooldown_config(tmp_path, settings),
|
||||||
|
remove_environment=("PROMETHEUS_MULTIPROC_DIR",),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
failing: Final = stack.enter_context(wire_server(failing_reply))
|
||||||
|
healthy: Final = stack.enter_context(wire_server(healthy_reply))
|
||||||
|
scenario: Final = stack.enter_context(owned.gateway.scenario())
|
||||||
|
lone_id: Final = deployment(scenario, lone_group, anthropic_params(failing))
|
||||||
|
failed: Final = tuple(raw_chat(owned.gateway, lone_group, new_marker()) for _ in range(4))
|
||||||
|
for outcome in failed:
|
||||||
|
assert_failed(outcome)
|
||||||
|
assert not cooled(cache, lone_id)
|
||||||
|
assert markers_received(failing) == markers(failed)
|
||||||
|
|
||||||
|
paired_id: Final = two_deployments(scenario, pair_group, failing, healthy)
|
||||||
|
history: Final = tuple(
|
||||||
|
itertools.takewhile(
|
||||||
|
lambda outcome: not cooled(cache, paired_id),
|
||||||
|
(raw_chat(owned.gateway, pair_group, new_marker()) for _ in range(40)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert cooled(cache, paired_id), [(outcome.status, outcome.detail[:80]) for outcome in history]
|
||||||
|
assert sum(outcome.status == 500 for outcome in history) == 1
|
||||||
|
served: Final = tuple(raw_chat(owned.gateway, pair_group, new_marker()) for _ in range(10))
|
||||||
|
assert_served(served, chat_identity)
|
||||||
|
assert markers_received(healthy) == (*markers(o for o in history if o.status == 200), *markers(served))
|
||||||
|
|
||||||
|
primary_id: Final = deployment(scenario, primary_group, anthropic_params(failing))
|
||||||
|
deployment(scenario, backup_group, openai_params(healthy))
|
||||||
|
failing.drain()
|
||||||
|
healthy.drain()
|
||||||
|
fell_back: Final = tuple(
|
||||||
|
itertools.takewhile(
|
||||||
|
lambda outcome: not cooled(cache, primary_id),
|
||||||
|
(raw_chat(owned.gateway, primary_group, new_marker()) for _ in range(40)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert cooled(cache, primary_id) and fell_back, [(o.status, o.detail[:80]) for o in fell_back]
|
||||||
|
assert_served(fell_back, chat_identity)
|
||||||
|
backed: Final = tuple(raw_chat(owned.gateway, primary_group, new_marker()) for _ in range(4))
|
||||||
|
assert_served(backed, chat_identity)
|
||||||
|
received: Final = markers_received(failing)
|
||||||
|
assert received[: len(fell_back)] == markers(fell_back)
|
||||||
|
assert len(received) <= len(fell_back) + 1, received
|
||||||
|
assert {*markers(fell_back), *markers(backed)} <= set(markers_received(healthy))
|
||||||
|
|
||||||
|
|
||||||
def test_a_deployment_allowed_fails_policy_cools_a_lone_402_deployment(rig: CooldownRig) -> None:
|
def test_a_deployment_allowed_fails_policy_cools_a_lone_402_deployment(rig: CooldownRig) -> None:
|
||||||
group: Final = new_group()
|
group: Final = new_group()
|
||||||
policy: Final = {"allowed_fails_policy": {"BadRequestErrorAllowedFails": 0}}
|
policy: Final = {"allowed_fails_policy": {"BadRequestErrorAllowedFails": 0}}
|
||||||
|
|
|
||||||
|
|
@ -2931,10 +2931,65 @@ async def test_bad_request_error_does_not_cool_down_a_deployment(
|
||||||
assert tuple(cooldown_deployments) == ()
|
assert tuple(cooldown_deployments) == ()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("use_policy", [False, True], ids=["allowed_fails", "allowed_fails_policy"])
|
TIMED_OUT_DEPLOYMENT: Final = {
|
||||||
|
"model_name": "test-model",
|
||||||
|
"litellm_params": {"model": "openai/test-model", "api_key": "test-key"},
|
||||||
|
"model_info": {"id": "test-deployment"},
|
||||||
|
}
|
||||||
|
BACKUP_DEPLOYMENT: Final = {
|
||||||
|
"model_name": "backup-model",
|
||||||
|
"litellm_params": {"model": "openai/backup-model", "api_key": "test-key", "api_base": "https://backup.test/v1"},
|
||||||
|
"model_info": {"id": "backup-deployment"},
|
||||||
|
}
|
||||||
|
BACKUP_REPLY: Final = {
|
||||||
|
"id": "chatcmpl-backup",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1700000000,
|
||||||
|
"model": "backup-model",
|
||||||
|
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
|
||||||
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _lone_call_outcome(router: Router) -> type[Exception]:
|
||||||
|
try:
|
||||||
|
await router.acompletion(model="test-model", messages=[{"role": "user", "content": "timeout test"}])
|
||||||
|
except (litellm.Timeout, RouterRateLimitError) as error:
|
||||||
|
return type(error)
|
||||||
|
pytest.fail("the timed-out deployment answered")
|
||||||
|
|
||||||
|
|
||||||
|
BENCHED: Final = (litellm.Timeout, litellm.Timeout, RouterRateLimitError, RouterRateLimitError, RouterRateLimitError)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("router_kwargs", "model_info", "outcomes"),
|
||||||
|
[
|
||||||
|
({"allowed_fails": 1}, {}, (litellm.Timeout,) * 5),
|
||||||
|
({"allowed_fails": 1}, {"allowed_fails_policy": {}}, (litellm.Timeout,) * 5),
|
||||||
|
({"allowed_fails_policy": AllowedFailsPolicy(RateLimitErrorAllowedFails=1)}, {}, (litellm.Timeout,) * 5),
|
||||||
|
({"allowed_fails_policy": AllowedFailsPolicy(TimeoutErrorAllowedFails=1)}, {}, BENCHED),
|
||||||
|
({"allowed_fails": 1, "fallbacks": [{"test-model": ["backup-model"]}]}, {}, BENCHED),
|
||||||
|
(
|
||||||
|
{"allowed_fails": 1, "fallbacks": [{"test-model": [{"model": "backup-model", "temperature": 0}]}]},
|
||||||
|
{},
|
||||||
|
BENCHED,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"allowed_fails",
|
||||||
|
"allowed_fails_with_an_empty_deployment_policy",
|
||||||
|
"policy_names_another_exception",
|
||||||
|
"policy_names_timeout",
|
||||||
|
"allowed_fails_with_a_fallback_group",
|
||||||
|
"allowed_fails_with_a_dict_fallback_target",
|
||||||
|
],
|
||||||
|
)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_allowed_fails_cools_a_single_deployment(
|
async def test_router_allowed_fails_benches_a_lone_deployment_only_when_the_policy_names_the_exception(
|
||||||
use_policy: bool,
|
router_kwargs: Mapping[str, object],
|
||||||
|
model_info: Mapping[str, object],
|
||||||
|
outcomes: tuple[type[Exception], ...],
|
||||||
respx_mock: respx.MockRouter,
|
respx_mock: respx.MockRouter,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -2942,38 +2997,52 @@ async def test_allowed_fails_cools_a_single_deployment(
|
||||||
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
|
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
|
||||||
side_effect=httpx.ReadTimeout("upstream timeout")
|
side_effect=httpx.ReadTimeout("upstream timeout")
|
||||||
)
|
)
|
||||||
policy_kwargs: Final = (
|
deployment: Final = {**TIMED_OUT_DEPLOYMENT, "model_info": {**TIMED_OUT_DEPLOYMENT["model_info"], **model_info}}
|
||||||
{"allowed_fails_policy": AllowedFailsPolicy(TimeoutErrorAllowedFails=1)}
|
router: Final = Router(model_list=[deployment], cooldown_time=300, num_retries=0, **router_kwargs)
|
||||||
if use_policy
|
|
||||||
else {"allowed_fails": 1}
|
seen: Final = tuple([await _lone_call_outcome(router) for _ in range(5)])
|
||||||
|
|
||||||
|
assert seen == outcomes
|
||||||
|
assert len(route.calls) == outcomes.count(litellm.Timeout)
|
||||||
|
|
||||||
|
|
||||||
|
async def _group_call_outcome(router: Router) -> str:
|
||||||
|
try:
|
||||||
|
response: Final = await router.acompletion(
|
||||||
|
model="test-model", messages=[{"role": "user", "content": "timeout test"}]
|
||||||
|
)
|
||||||
|
except litellm.Timeout:
|
||||||
|
return "timeout"
|
||||||
|
return response.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_router_allowed_fails_still_benches_a_deployment_that_has_a_sibling_in_its_group(
|
||||||
|
respx_mock: respx.MockRouter,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||||
|
timed_out: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
|
||||||
|
side_effect=httpx.ReadTimeout("upstream timeout")
|
||||||
)
|
)
|
||||||
|
respx_mock.post("https://backup.test/v1/chat/completions").mock(return_value=httpx.Response(200, json=BACKUP_REPLY))
|
||||||
router: Final = Router(
|
router: Final = Router(
|
||||||
model_list=[
|
model_list=[TIMED_OUT_DEPLOYMENT, {**BACKUP_DEPLOYMENT, "model_name": "test-model"}],
|
||||||
{
|
|
||||||
"model_name": "test-model",
|
|
||||||
"litellm_params": {"model": "openai/test-model", "api_key": "test-key"},
|
|
||||||
"model_info": {"id": "test-deployment"},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
cooldown_time=300,
|
cooldown_time=300,
|
||||||
num_retries=0,
|
num_retries=0,
|
||||||
**policy_kwargs,
|
allowed_fails=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
for _ in range(2):
|
while len(timed_out.calls) < 2:
|
||||||
with pytest.raises(litellm.Timeout):
|
await _group_call_outcome(router)
|
||||||
await router.acompletion(
|
|
||||||
model="test-model",
|
|
||||||
messages=[{"role": "user", "content": "timeout test"}],
|
|
||||||
)
|
|
||||||
|
|
||||||
cooldown_deployments: Final = await async_get_cooldown_deployments(
|
cooldown_deployments: Final = await async_get_cooldown_deployments(
|
||||||
litellm_router_instance=router,
|
litellm_router_instance=router, parent_otel_span=None
|
||||||
parent_otel_span=None,
|
|
||||||
)
|
)
|
||||||
|
served: Final = tuple([await _group_call_outcome(router) for _ in range(5)])
|
||||||
|
|
||||||
assert len(route.calls) == 2
|
|
||||||
assert tuple(cooldown_deployments) == ("test-deployment",)
|
assert tuple(cooldown_deployments) == ("test-deployment",)
|
||||||
|
assert served == ("chatcmpl-backup",) * 5
|
||||||
|
assert len(timed_out.calls) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_per_user_session_upstream_errors_never_cool_down_the_shared_deployment(single_deployment_router):
|
def test_per_user_session_upstream_errors_never_cool_down_the_shared_deployment(single_deployment_router):
|
||||||
|
|
|
||||||
|
|
@ -1255,7 +1255,12 @@ class TestTriggerCooldownForFailedDeployment:
|
||||||
"model_name": "fallback-model",
|
"model_name": "fallback-model",
|
||||||
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
||||||
"model_info": {"id": "fallback-deployment"},
|
"model_info": {"id": "fallback-deployment"},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"model_name": "fallback-model",
|
||||||
|
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
||||||
|
"model_info": {"id": "fallback-sibling"},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
allowed_fails=0,
|
allowed_fails=0,
|
||||||
cooldown_time=60,
|
cooldown_time=60,
|
||||||
|
|
@ -1345,7 +1350,12 @@ async def test_a_fallback_deployment_whose_stream_stalled_stays_out_of_cooldown(
|
||||||
"model_name": "fallback-model",
|
"model_name": "fallback-model",
|
||||||
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
||||||
"model_info": {"id": "fallback-deployment"},
|
"model_info": {"id": "fallback-deployment"},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"model_name": "fallback-model",
|
||||||
|
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
||||||
|
"model_info": {"id": "fallback-sibling"},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
allowed_fails=0,
|
allowed_fails=0,
|
||||||
cooldown_time=60,
|
cooldown_time=60,
|
||||||
|
|
|
||||||
|
|
@ -12212,7 +12212,12 @@ class TestBackgroundResponseCostPollCooldown:
|
||||||
"model_name": "gpt-4.1",
|
"model_name": "gpt-4.1",
|
||||||
"litellm_params": {"model": "openai/gpt-4.1"},
|
"litellm_params": {"model": "openai/gpt-4.1"},
|
||||||
"model_info": {"id": "dep-1"},
|
"model_info": {"id": "dep-1"},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-4.1",
|
||||||
|
"litellm_params": {"model": "openai/gpt-4.1"},
|
||||||
|
"model_info": {"id": "dep-2"},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
allowed_fails=0,
|
allowed_fails=0,
|
||||||
)
|
)
|
||||||
|
|
@ -12340,7 +12345,12 @@ class TestCallerTimeoutCooldown:
|
||||||
"model_name": "slow-model",
|
"model_name": "slow-model",
|
||||||
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
||||||
"model_info": {"id": "dep-1"},
|
"model_info": {"id": "dep-1"},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"model_name": "slow-model",
|
||||||
|
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"},
|
||||||
|
"model_info": {"id": "dep-2"},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
allowed_fails=0,
|
allowed_fails=0,
|
||||||
cooldown_time=120,
|
cooldown_time=120,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue