fix(router): warn once per offending config and expose routing_strategy_args in the UI

This commit is contained in:
Tin Chi Lo 2026-08-06 14:00:11 -07:00
parent 92d8a35b60
commit b92b9ae504
6 changed files with 139 additions and 25 deletions

View file

@ -715,7 +715,7 @@ class Router:
self._init_routing_groups(self._routing_groups_input)
self._override_selectors: dict[str, Any] = {}
self._override_selectors_lock = threading.Lock()
self._warned_model_group_strategy_models: set[str] = set() # mutable-ok: warn-once misconfig registry
self._warned_model_group_strategy_keys: set[tuple[str, str, str]] = set() # mutable-ok: warn-once registry
self.access_groups = None
## USAGE TRACKING ##
if isinstance(litellm._async_success_callback, list):
@ -1129,25 +1129,45 @@ class Router:
_EMPTY_MAPPING: Final[Mapping[str, object]] = MappingProxyType({})
def _warn_model_group_strategy_once(self, model: str, kind: str, fingerprint: str, message: str) -> None:
"""
Reports a model-group strategy misconfiguration once per
(model, problem kind, offending config). Distinct problem kinds warn
independently, and a changed config re-warns.
"""
warn_key: Final = (model, kind, fingerprint)
if warn_key in self._warned_model_group_strategy_keys:
return
self._warned_model_group_strategy_keys.add(warn_key)
verbose_router_logger.warning("%s", message)
def _deployment_strategy_entry(self, idx: int) -> tuple[str | None, Mapping[str, object]] | None:
"""
The (normalized strategy, args) a deployment declares via
`model_info.routing_strategy`, or None when unset. An empty string
counts as unset, so a PATCH (which cannot delete a model_info key) can
still clear the field.
"""
info: Final = self.model_list[idx].get("model_info") or self._EMPTY_MAPPING
raw: Final = info.get("routing_strategy")
if not raw:
return None
normalized: Final = self._normalize_strategy(raw) if isinstance(raw, (str, RoutingStrategy)) else None
return normalized, info.get("routing_strategy_args") or self._EMPTY_MAPPING
def _get_model_group_strategy_config(self, model: str) -> tuple[str, Mapping[str, object]] | None:
"""
Reads `model_info.routing_strategy` (+ `routing_strategy_args`) off the
deployments of `model`. When deployments of the same model_name disagree,
the first deployment in model_list order wins; invalid or conflicting
values are reported once per model_name so a bad stored value can never
take down that model's traffic. An empty string counts as unset, so a
PATCH (which cannot delete a model_info key) can still clear the field.
values are reported once per offending config so a bad stored value can
never take down that model's traffic.
"""
indices: Final = self.model_name_to_deployment_indices.get(model)
if not indices:
return None
configured: Final = tuple(
(
self._normalize_strategy(raw) if isinstance(raw, (str, RoutingStrategy)) else None,
info.get("routing_strategy_args") or self._EMPTY_MAPPING,
)
for idx in indices
if (raw := (info := self.model_list[idx].get("model_info") or self._EMPTY_MAPPING).get("routing_strategy"))
entry for idx in indices if (entry := self._deployment_strategy_entry(idx)) is not None
)
if not configured:
return None
@ -1158,14 +1178,15 @@ class Router:
)
has_invalid: Final = len(valid) < len(configured)
has_conflict: Final = len(frozenset(strategy for strategy, _ in valid)) > 1
if (has_invalid or has_conflict) and model not in self._warned_model_group_strategy_models:
self._warned_model_group_strategy_models.add(model)
verbose_router_logger.warning(
"model_info.routing_strategy for model_group '%s' has %s; using '%s'. Supported strategies: %s.",
if has_invalid or has_conflict:
self._warn_model_group_strategy_once(
model,
"an unsupported value" if has_invalid else "conflicting values across deployments",
valid[0][0] if valid else self._normalize_strategy(self.routing_strategy),
sorted(self._OVERRIDABLE_ROUTING_STRATEGIES),
"config",
",".join(str(strategy) for strategy, _ in configured),
f"model_info.routing_strategy for model_group '{model}' has "
f"{'an unsupported value' if has_invalid else 'conflicting values across deployments'}; using "
f"'{valid[0][0] if valid else self._normalize_strategy(self.routing_strategy)}'. "
f"Supported strategies: {sorted(self._OVERRIDABLE_ROUTING_STRATEGIES)}.",
)
return valid[0] if valid else None
@ -1207,14 +1228,13 @@ class Router:
if selector_key not in self._override_selectors:
built: Final = self._build_model_group_selector(strategy, args)
if built is None:
if model not in self._warned_model_group_strategy_models:
self._warned_model_group_strategy_models.add(model)
verbose_router_logger.warning(
"model_info.routing_strategy_args for model_group '%s' cannot initialize strategy "
"'%s'; falling back to the routing-group / top-level strategy.",
model,
strategy,
)
self._warn_model_group_strategy_once(
model,
"args",
selector_key,
f"model_info.routing_strategy_args for model_group '{model}' cannot initialize strategy "
f"'{strategy}'; falling back to the routing-group / top-level strategy.",
)
return None
live_keys: Final = self._live_model_group_selector_keys()
stale: Final = tuple(k for k in self._override_selectors if "|" in k and k not in live_keys)

View file

@ -159,6 +159,48 @@ def test_invalid_args_fall_back_without_failing_traffic(caplog):
assert len(warnings) == 1
def test_config_and_args_warnings_fire_independently(caplog):
router = _build_router(
[
_deployment(
"quality",
"openai/gpt-4o",
"d1",
{"routing_strategy": "latency-based-routing", "routing_strategy_args": {"ttl": "bogus"}},
),
_deployment("quality", "openai/gpt-4o-mini", "d2", {"routing_strategy": "cost-based-routing"}),
]
)
with caplog.at_level(logging.WARNING, logger="LiteLLM Router"):
strategy, _ = router._get_routing_context("quality")
router._get_routing_context("quality")
assert strategy == "simple-shuffle"
conflict_warnings = [r for r in caplog.records if "conflicting values" in r.getMessage()]
args_warnings = [r for r in caplog.records if "cannot initialize strategy" in r.getMessage()]
assert len(conflict_warnings) == 1
assert len(args_warnings) == 1
def test_changed_bad_args_warn_again():
router = _build_router(
[
_deployment(
"quality",
"openai/gpt-4o",
"d1",
{"routing_strategy": "latency-based-routing", "routing_strategy_args": {"ttl": "bogus"}},
)
]
)
router._get_routing_context("quality")
assert len(router._warned_model_group_strategy_keys) == 1
for idx in router.model_name_to_deployment_indices["quality"]:
router.model_list[idx]["model_info"]["routing_strategy_args"] = {"ttl": "still-bogus"}
router._get_routing_context("quality")
assert len(router._warned_model_group_strategy_keys) == 2
def test_simple_shuffle_with_args_keeps_shuffle_semantics(caplog):
router = _build_router(
[

View file

@ -193,6 +193,16 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
<Select allowClear placeholder="Inherit router default" options={[...ROUTING_STRATEGY_OPTIONS]} />
</Form.Item>
<Form.Item
label="Routing Strategy Args"
name="routing_strategy_args"
className="mb-4"
rules={[{ validator: formItemValidateJSON }]}
help="Optional JSON args for the selected strategy, e.g. latency window TTL."
>
<TextArea rows={2} placeholder='{"ttl": 3600}' />
</Form.Item>
<Form.Item label="Tags" name="tags" className="mb-4">
<Select
mode="tags"

View file

@ -66,6 +66,7 @@ describe("prepareModelAddRequest", () => {
],
model_name: "bedrock/claude-opus",
routing_strategy: "cost-based-routing",
routing_strategy_args: '{"ttl": 3600}',
};
const deployments = await prepareModelAddRequest({ ...formValues }, "token", null);
@ -73,7 +74,9 @@ describe("prepareModelAddRequest", () => {
expect(deployments).toHaveLength(1);
const [deployment] = deployments!;
expect(deployment.modelInfoObj.routing_strategy).toBe("cost-based-routing");
expect(deployment.modelInfoObj.routing_strategy_args).toEqual({ ttl: 3600 });
expect(deployment.litellmParamsObj.routing_strategy).toBeUndefined();
expect(deployment.litellmParamsObj.routing_strategy_args).toBeUndefined();
});
it("ignores litellm_credential_name inside LiteLLM Params JSON", async () => {

View file

@ -111,6 +111,13 @@ export const prepareModelAddRequest = async (formValues: Record<string, any>, ac
modelInfoObj["team_id"] = value;
} else if (key === "routing_strategy") {
modelInfoObj["routing_strategy"] = value;
} else if (key === "routing_strategy_args") {
try {
modelInfoObj["routing_strategy_args"] = JSON.parse(value as string);
} catch (error) {
NotificationManager.fromBackend("Failed to parse Routing Strategy Args: " + error);
throw new Error("Failed to parse routing_strategy_args: " + error);
}
} else if (key === "model_access_group") {
modelInfoObj["access_groups"] = value;
} else if (key == "mode") {

View file

@ -435,6 +435,16 @@ export default function ModelInfoView({
routing_strategy: formRoutingStrategy,
};
}
if (values.routing_strategy_args !== undefined) {
const parsedArgs = values.routing_strategy_args ? JSON.parse(values.routing_strategy_args) : {};
const storedArgs = modelData.model_info?.routing_strategy_args ?? {};
if (JSON.stringify(parsedArgs) !== JSON.stringify(storedArgs)) {
updatedModelInfo = {
...updatedModelInfo,
routing_strategy_args: parsedArgs,
};
}
}
} catch (e) {
NotificationsManager.fromBackend("Invalid JSON in Model Info");
return;
@ -809,6 +819,9 @@ export default function ModelInfoView({
tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [],
health_check_model: isWildcardModel ? localModelData.model_info?.health_check_model : null,
routing_strategy: localModelData.model_info?.routing_strategy || "",
routing_strategy_args: localModelData.model_info?.routing_strategy_args
? JSON.stringify(localModelData.model_info.routing_strategy_args)
: "",
litellm_credential_name: localModelData.litellm_params?.litellm_credential_name || "",
litellm_extra_params: JSON.stringify(
Object.fromEntries(
@ -1096,6 +1109,25 @@ export default function ModelInfoView({
)}
</div>
<div>
<Text className="font-medium">Routing Strategy Args</Text>
{isEditing ? (
<Form.Item
name="routing_strategy_args"
className="mb-0"
rules={[{ validator: formItemValidateJSON }]}
>
<Input placeholder='{"ttl": 3600}' />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded-sm">
{localModelData.model_info?.routing_strategy_args
? JSON.stringify(localModelData.model_info.routing_strategy_args)
: "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">
Guardrails