Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/lar1_ollama_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
model_list:
- model_name: agent-router
litellm_params:
model: ollama/qwen3.5:9b
api_base: http://127.0.0.1:11434
model_info:
id: cloud-smart
type: cloud-smart

- model_name: agent-router
litellm_params:
model: ollama/phi4-mini:latest
api_base: http://127.0.0.1:11434
model_info:
id: cloud-fast
type: cloud-fast

- model_name: agent-router
litellm_params:
model: ollama/llama3.2:3b
api_base: http://127.0.0.1:11434
model_info:
id: local
type: local

- model_name: agent-router
litellm_params:
model: ollama/lfm2.5-thinking:latest
api_base: http://127.0.0.1:11434
model_info:
id: deep
type: deep

router_settings:
routing_strategy: lar1
routing_strategy_args:
confidence_threshold_low: 0.3
confidence_threshold_medium: 0.5
confidence_threshold_high: 0.7

general_settings:
master_key: sk-lar1-demo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Known master key in executable example

Anyone who can reach a proxy started from this example can authenticate with the repository-known master key and access administrative routes and configured models. Require the operator to provide a secret instead.

Suggested change
master_key: sk-lar1-demo
master_key: os.environ/LITELLM_MASTER_KEY


litellm_settings:
set_verbose: true
51 changes: 40 additions & 11 deletions litellm/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def __init__(
"latency-based-routing",
"cost-based-routing",
"usage-based-routing-v2",
"lar1",
] = "simple-shuffle",
optional_pre_call_checks: Optional[OptionalPreCallChecks] = None,
routing_strategy_args: dict = {}, # just for latency-based
Expand Down Expand Up @@ -647,10 +648,15 @@ def __init__(
"""

### ROUTING SETUP ###
self.routing_strategy_init(
routing_strategy=routing_strategy,
routing_strategy_args=routing_strategy_args,
)
if self._normalize_strategy(routing_strategy) == "lar1":
from litellm.router_strategy.lar1_routing import apply_lar1_routing_strategy

apply_lar1_routing_strategy(self, routing_strategy_args)
else:
self.routing_strategy_init(
routing_strategy=routing_strategy,
routing_strategy_args=routing_strategy_args,
)
self._init_routing_groups(self._routing_groups_input)
self.access_groups = None
## USAGE TRACKING ##
Expand Down Expand Up @@ -871,7 +877,9 @@ def _validate_routing_strategy(
self, routing_strategy: Union[RoutingStrategy, str, None]
) -> None:
# See: https://github.com/BerriAI/litellm/issues/11330
valid_strategy_strings = ["simple-shuffle"] + [s.value for s in RoutingStrategy]
valid_strategy_strings = ["simple-shuffle", "lar1"] + [
s.value for s in RoutingStrategy
]
if routing_strategy is None:
return
is_valid_string = (
Expand Down Expand Up @@ -10583,6 +10591,7 @@ def update_settings(self, **kwargs):

_existing_router_settings = self.get_settings()
rebuild_routing_groups = False
relink_lar1_from_args = False
for var in kwargs:
if var in _allowed_settings:
if var in _int_settings:
Expand All @@ -10597,17 +10606,37 @@ def update_settings(self, **kwargs):
if var == "routing_strategy":
value = self._normalize_strategy(value)
if _existing_router_settings["routing_strategy"] != value:
self.routing_strategy_init(
routing_strategy=value,
routing_strategy_args=kwargs.get(
"routing_strategy_args", {}
),
)
if value == "lar1":
from litellm.router_strategy.lar1_routing import (
apply_lar1_routing_strategy,
)

apply_lar1_routing_strategy(
self,
kwargs.get("routing_strategy_args"),
)
else:
self.routing_strategy_init(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Disabling LAR-1 leaves its selector active

apply_lar1_routing_strategy() shadows both deployment-selection methods on the router instance, but this branch does not remove those attributes when switching away from LAR-1. An authenticated caller can therefore continue forcing cloud-smart or deep deployments with crafted metadata.lar1 after an operator changes the strategy to simple-shuffle or another policy. Restore the class methods before initializing the replacement strategy, or integrate LAR-1 into the normal strategy-selector dispatch rather than monkey-patching the router.

routing_strategy=value,
routing_strategy_args=kwargs.get(
"routing_strategy_args", {}
),
)
rebuild_routing_groups = True
elif var == "routing_strategy_args":
relink_lar1_from_args = True
setattr(self, var, value)
else:
verbose_router_logger.debug("Setting {} is not allowed".format(var))

if (
relink_lar1_from_args
and self._normalize_strategy(self.routing_strategy) == "lar1"
):
from litellm.router_strategy.lar1_routing import apply_lar1_routing_strategy

apply_lar1_routing_strategy(self, self.routing_strategy_args)

if rebuild_routing_groups:
self._init_routing_groups(self._routing_groups_input)
verbose_router_logger.debug(f"Updated Router settings: {self.get_settings()}")
Expand Down
193 changes: 193 additions & 0 deletions litellm/router_strategy/lar1_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""
LAR-1 Semantic Routing Strategy

Routes requests based on agent confidence level (LAR-1 protocol).
Thresholds are configurable via routing_strategy_args in router config.

LAR-1 metadata passed via request_kwargs["metadata"]["lar1"]
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING, Optional, Union

from pydantic import ValidationError

from litellm._logging import verbose_router_logger
from litellm.router import CustomRoutingStrategyBase
from litellm.types.lar1 import LAR1Metadata, LAR1Time

if TYPE_CHECKING:
from litellm.router import Router

DEFAULT_THRESHOLDS: dict[str, float] = {"low": 0.3, "medium": 0.5, "high": 0.7}


def _coerce_threshold(value: object, default: float) -> float:
if isinstance(value, (int, float)):
return float(value)
return default


def lar1_thresholds_from_args(
routing_strategy_args: Optional[Mapping[str, object]] = None,
) -> dict[str, float]:
args = routing_strategy_args or {}
return {
"low": _coerce_threshold(
args.get("confidence_threshold_low"), DEFAULT_THRESHOLDS["low"]
),
"medium": _coerce_threshold(
args.get("confidence_threshold_medium"), DEFAULT_THRESHOLDS["medium"]
),
"high": _coerce_threshold(
args.get("confidence_threshold_high"), DEFAULT_THRESHOLDS["high"]
),
}


def apply_lar1_routing_strategy(
router: Router,
routing_strategy_args: Optional[Mapping[str, object]] = None,
) -> None:
router.routing_strategy = "lar1"
router.set_custom_routing_strategy(
LAR1RoutingStrategy(
router_instance=router,
thresholds=lar1_thresholds_from_args(routing_strategy_args),
)
)


def _normalize_thresholds(thresholds: Optional[dict[str, float]]) -> dict[str, float]:
merged = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
low = merged["low"]
medium = merged["medium"]
high = merged["high"]
if not (0 < low < medium < high < 1):
raise ValueError(
"LAR-1 thresholds must satisfy 0 < low < medium < high < 1, "
f"got low={low}, medium={medium}, high={high}"
)
return merged


def _parse_lar1_metadata(request_kwargs: dict) -> LAR1Metadata:
lar1_raw = request_kwargs.get("metadata", {}).get("lar1", {})
if not isinstance(lar1_raw, dict):
verbose_router_logger.warning(
f"[LAR-1] Invalid lar1 metadata type: {type(lar1_raw).__name__}. Using defaults"
)
return LAR1Metadata()
try:
return LAR1Metadata.model_validate(lar1_raw)
except ValidationError as exc:
verbose_router_logger.warning(
f"[LAR-1] Invalid lar1 metadata: {exc}. Using defaults"
)
return LAR1Metadata()


class LAR1RoutingStrategy(CustomRoutingStrategyBase):
def __init__(
self,
router_instance: Optional[Router] = None,
thresholds: Optional[dict[str, float]] = None,
):
self._router = router_instance
self.thresholds = _normalize_thresholds(thresholds)

async def async_get_available_deployment(
self,
model: str,
messages: Optional[list[dict[str, str]]] = None,
input: Optional[Union[str, list]] = None,
specific_deployment: Optional[bool] = False,
request_kwargs: Optional[dict] = None,
):
if request_kwargs is None:
request_kwargs = {}
if self._router is None:
return None

lar1 = _parse_lar1_metadata(request_kwargs)
confidence = lar1.confidence
evidence = tuple(e.value for e in lar1.evidence)
time_dim = lar1.time.value

healthy = await self._router.async_get_healthy_deployments(
model=model,
request_kwargs=request_kwargs,
messages=messages,
input=input,
specific_deployment=specific_deployment,
)
if isinstance(healthy, dict):
return healthy

if not healthy:
return None

target = self._classify_request(confidence, evidence, time_dim)
selected, exact_match = self._select_deployment(target, healthy)

if selected is None:
return None
if exact_match:
verbose_router_logger.info(f"[LAR-1] confidence={confidence} -> {target}")
else:
actual_type = selected.get("model_info", {}).get("type", "unknown")
verbose_router_logger.warning(
f"[LAR-1] No deployment for type '{target}', "
f"fallback to deployment type '{actual_type}'"
)
return selected

def _classify_request(
self,
confidence: float,
evidence: tuple[str, ...],
time_dim: str,
) -> str:
if "UNVERIFIED" in evidence:
return "cloud-smart"

if time_dim == LAR1Time.MEM.value:
return "cloud-fast"

t = self.thresholds
if confidence < t["low"]:
return "cloud-smart"
if confidence < t["medium"]:
return "cloud-fast"
if confidence < t["high"]:
return "local"
return "deep"

def _select_deployment(
self,
target_type: str,
deployments: list[dict],
) -> tuple[Optional[dict], bool]:
if not deployments:
return None, False

for deployment in deployments:
if not isinstance(deployment, dict):
continue
model_type = deployment.get("model_info", {}).get("type", "")
if model_type == target_type:
return deployment, True

for deployment in deployments:
if isinstance(deployment, dict):
return deployment, False

return None, False

def get_available_deployment(self, *args, **kwargs):
raise NotImplementedError(
"LAR-1 routing only supports async routing. "
"Enable async_only_mode on the router or use acompletion."
)
39 changes: 39 additions & 0 deletions litellm/types/lar1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from enum import Enum

from pydantic import BaseModel, Field


class LAR1Act(str, Enum):
INF = "INF"
OBS = "OBS"
RET = "RET"
GEN = "GEN"


class LAR1Time(str, Enum):
NOW = "NOW"
MEM = "MEM"
CTX = "CTX"
PRE = "PRE"


class LAR1Mind(str, Enum):
REF = "REF"
REC = "REC"
HYP = "HYP"
ACT = "ACT"


class LAR1Evidence(str, Enum):
SYNTH = "SYNTH"
RETRIEVED = "RETRIEVED"
UNVERIFIED = "UNVERIFIED"
CONFIRMED = "CONFIRMED"


class LAR1Metadata(BaseModel):
act: LAR1Act = LAR1Act.INF
time: LAR1Time = LAR1Time.NOW
mind: LAR1Mind = LAR1Mind.REF
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
evidence: list[LAR1Evidence] = []
Loading
Loading