Skip to content

Commit 649351a

Browse files
author
Paweł Kędzia
committed
Merge branch 'features/quickstart'
2 parents 3a33a96 + c5ea26a commit 649351a

File tree

6 files changed

+220
-18
lines changed

6 files changed

+220
-18
lines changed

llm_router_api/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Configuration is driven primarily by environment variables and a JSON model‑co
6565
| `LLM_ROUTER_BALANCE_STRATEGY` | Strategy used to balance routing between LLM providers. Allowed values are `balanced`, `weighted`, `dynamic_weighted` (beta), `first_available` and `first_available_optim` as defined in `constants_base.py`. | `balanced` |
6666
| `LLM_ROUTER_REDIS_HOST` | Redis host for load‑balancing when a multi‑provider model is available. | `<empty string>` |
6767
| `LLM_ROUTER_REDIS_PORT` | Redis port for load‑balancing when a multi‑provider model is available. | `6379` |
68+
| `LLM_ROUTER_REDIS_PASSWORD` | Password for Redis connection. | `<not set>` |
69+
| `LLM_ROUTER_REDIS_DB` | Redis database number. | `0` |
6870
| `LLM_ROUTER_SERVER_TYPE` | Server implementation to use (`flask`, `gunicorn`, `waitress`). | `flask` |
6971
| `LLM_ROUTER_SERVER_PORT` | Port on which the server listens. | `8080` |
7072
| `LLM_ROUTER_SERVER_HOST` | Host address for the server. | `0.0.0.0` |

llm_router_api/base/constants.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
# Run service as a proxy only
5252
SERVICE_AS_PROXY = bool_env_value(f"{_DontChangeMe.MAIN_ENV_PREFIX}MINIMUM")
5353

54+
# =============================================================================
55+
# SERVER CONFIGURATION
56+
# =============================================================================
5457
# Type of server, default is flask {flask, gunicorn, waitress}
5558
SERVER_TYPE = (
5659
os.environ.get(f"{_DontChangeMe.MAIN_ENV_PREFIX}SERVER_TYPE", "flask")
@@ -95,19 +98,32 @@
9598
if RUN_IN_DEBUG_MODE:
9699
REST_API_LOG_LEVEL = "DEBUG"
97100

101+
# =============================================================================
98102
# Use Prometheus to collect metrics
99103
USE_PROMETHEUS = bool_env_value(f"{_DontChangeMe.MAIN_ENV_PREFIX}USE_PROMETHEUS")
100104

105+
# =============================================================================
101106
# Strategy for load balancing when a multi-provider model is available
102107
SERVER_BALANCE_STRATEGY = os.environ.get(
103108
f"{_DontChangeMe.MAIN_ENV_PREFIX}BALANCE_STRATEGY", BalanceStrategies.BALANCED
104109
).strip()
105110

111+
# =============================================================================
112+
# REDIS CONFIGURATION
113+
# =============================================================================
106114
# Strategy for load balancing when a multi-provider model is available
107115
REDIS_HOST = os.environ.get(f"{_DontChangeMe.MAIN_ENV_PREFIX}REDIS_HOST", "").strip()
108-
109116
# Strategy for load balancing when a multi-provider model is available
110117
REDIS_PORT = int(os.environ.get(f"{_DontChangeMe.MAIN_ENV_PREFIX}REDIS_PORT", 6379))
118+
# Redis database number
119+
REDIS_DB = int(os.environ.get(f"{_DontChangeMe.MAIN_ENV_PREFIX}REDIS_DB", 0))
120+
# Redis password
121+
REDIS_PASSWORD = os.environ.get(
122+
f"{_DontChangeMe.MAIN_ENV_PREFIX}REDIS_PASSWORD", ""
123+
).strip()
124+
if not len(REDIS_PASSWORD):
125+
REDIS_PASSWORD = None
126+
111127

112128
# =============================================================================
113129
# MASKING
@@ -132,6 +148,7 @@
132148
for _s in MASKING_STRATEGY_PIPELINE.strip().split(",")
133149
if len(_s.strip())
134150
]
151+
135152
# =============================================================================
136153
# GUARDRAILS
137154
# =============================================================================

llm_router_api/core/lb/redis_based_interface.py

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
"""
2+
Redis based strategy interface.
3+
4+
This module defines an abstract base class that combines load‑balancing
5+
strategy selection with Redis‑backed health‑checking. It is used by the
6+
router to coordinate provider allocation across multiple processes or
7+
hosts, guaranteeing that a particular provider is assigned to at most one
8+
consumer at any time.
9+
10+
The implementation relies on Lua scripts for atomic acquire/release
11+
operations and uses a per‑model Redis hash to store lock flags.
12+
"""
13+
114
import random
215
import logging
316

@@ -11,7 +24,12 @@
1124
except ImportError:
1225
REDIS_IS_AVAILABLE = False
1326

14-
from llm_router_api.base.constants import REDIS_PORT, REDIS_HOST
27+
from llm_router_api.base.constants import (
28+
REDIS_PORT,
29+
REDIS_HOST,
30+
REDIS_DB,
31+
REDIS_PASSWORD,
32+
)
1533
from llm_router_api.core.lb.strategy_interface import ChooseProviderStrategyI
1634
from llm_router_api.core.monitor.redis_health_interface import (
1735
RedisBasedHealthCheckInterface,
@@ -24,22 +42,34 @@ class RedisBasedStrategyInterface(
2442
"""
2543
Strategy that selects the first free provider for a model using Redis.
2644
27-
The class inherits from
28-
:class:`~llm_router_api.core.lb.strategy.ChooseProviderStrategyI`
29-
and adds Redis‑based coordination. It ensures that at most one consumer
30-
holds a particular provider at any time, even when multiple workers run
31-
concurrently on different hosts.
32-
33-
Parameters are forwarded to the base class where appropriate, and Redis
34-
connection details can be customised via the constructor arguments.
45+
This class merges two responsibilities:
46+
47+
* **Load‑balancing** – Implements the
48+
:class:`~llm_router_api.core.lb.strategy.ChooseProviderStrategyI`
49+
contract, choosing a provider for a given model.
50+
* **Health‑checking** – Inherits from
51+
:class:`~llm_router_api.core.monitor.redis_health_interface.RedisBasedHealthCheckInterface`
52+
to monitor provider health via Redis.
53+
54+
By storing a per‑model hash in Redis where each field represents a
55+
provider’s lock flag, the strategy guarantees that only one worker can
56+
acquire a provider at a time, even when the workers run on different
57+
machines. The acquisition and release are performed atomically using
58+
Lua scripts, eliminating race conditions.
59+
60+
The constructor accepts optional Redis connection parameters so the
61+
strategy can be pointed at any Redis instance, and a ``strategy_prefix``
62+
can be used to namespace keys when multiple strategies share the same
63+
Redis server.
3564
"""
3665

3766
def __init__(
3867
self,
3968
models_config_path: str,
4069
redis_host: str = REDIS_HOST,
70+
redis_password: str = REDIS_PASSWORD,
4171
redis_port: int = REDIS_PORT,
42-
redis_db: int = 0,
72+
redis_db: int = REDIS_DB,
4373
timeout: int = 60,
4474
check_interval: float = 0.1,
4575
monitor_check_interval: float = 30,
@@ -82,6 +112,7 @@ def __init__(
82112
redis_host=redis_host,
83113
redis_port=redis_port,
84114
redis_db=redis_db,
115+
redis_password=redis_password,
85116
clear_buffers=clear_buffers,
86117
logger=logger,
87118
check_interval=monitor_check_interval,
@@ -127,6 +158,32 @@ def init_provider(
127158
providers: List[Dict],
128159
options: Optional[Dict[str, Any]] = None,
129160
) -> Tuple[str | None, bool]:
161+
"""
162+
Prepare Redis structures for a model and optionally enable random choice.
163+
164+
This method is invoked once per model during strategy start‑up. It
165+
registers the providers with the monitoring subsystem, ensures that a
166+
Redis hash exists for the model, and populates the hash fields with a
167+
``'false'`` value (meaning *free*) if the hash is missing. The returned
168+
``redis_key`` is the base key used for all subsequent lock operations.
169+
170+
Parameters
171+
----------
172+
model_name: str
173+
The logical name of the model (e.g., ``"gpt‑4"``).
174+
providers: List[Dict]
175+
A list of provider configuration dictionaries.
176+
options: dict | None, optional
177+
If ``options.get("random_choice")`` is true, the caller intends to
178+
acquire a provider at random; the flag is propagated to the caller.
179+
180+
Returns
181+
-------
182+
Tuple[str | None, bool]
183+
``(redis_key, is_random)`` where ``redis_key`` is the Redis hash key
184+
for the model (or ``None`` if ``providers`` is empty) and ``is_random``
185+
reflects the ``random_choice`` option.
186+
"""
130187
if not providers:
131188
return None, False
132189
# Register providers for monitoring (only once per model)
@@ -153,6 +210,24 @@ def _get_redis_key(self, model_name: str) -> str:
153210
return f"model:{model_name}"
154211

155212
def _host_key(self, host_name: str) -> str:
213+
"""
214+
Return a Redis key that uniquely identifies a host.
215+
216+
Host names may contain characters that are unsuitable for Redis keys.
217+
This method sanitises the name by replacing any character listed in
218+
``self.REPLACE_PROVIDER_KEY`` with an underscore and then prefixes it
219+
with ``"host:"``.
220+
221+
Parameters
222+
----------
223+
host_name: str
224+
The raw host identifier (e.g., a hostname or IP address).
225+
226+
Returns
227+
-------
228+
str
229+
A safe Redis key such as ``"host:my_server_01"``.
230+
"""
156231
for ch in self.REPLACE_PROVIDER_KEY:
157232
host_name = host_name.replace(ch, "_")
158233

@@ -266,6 +341,26 @@ def _try_acquire_random_provider(
266341
def _get_active_providers(
267342
self, model_name: str, providers: List[Dict]
268343
) -> List[Dict]:
344+
"""
345+
Retrieve the list of currently active providers for a model.
346+
347+
The method delegates to the monitoring component, which tracks the
348+
health status of each provider. Only providers whose health check
349+
reports *active* are returned.
350+
351+
Parameters
352+
----------
353+
model_name: str
354+
The logical name of the model.
355+
providers: List[Dict]
356+
The full provider configuration list (kept for signature compatibility;
357+
it is not used directly).
358+
359+
Returns
360+
-------
361+
List[Dict]
362+
A list containing the configuration dictionaries of active providers.
363+
"""
269364
active_providers = self._monitor.get_providers(
270365
model_name=model_name, only_active=True
271366
)

llm_router_api/core/lb/strategies/first_available.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@
3434

3535
from typing import List, Dict, Optional, Any
3636

37-
from llm_router_api.base.constants import REDIS_PORT, REDIS_HOST
37+
from llm_router_api.base.constants import (
38+
REDIS_PORT,
39+
REDIS_HOST,
40+
REDIS_PASSWORD,
41+
REDIS_DB,
42+
)
3843
from llm_router_api.core.lb.redis_based_interface import RedisBasedStrategyInterface
3944

4045

@@ -56,8 +61,9 @@ def __init__(
5661
self,
5762
models_config_path: str,
5863
redis_host: str = REDIS_HOST,
64+
redis_password: str = REDIS_PASSWORD,
5965
redis_port: int = REDIS_PORT,
60-
redis_db: int = 0,
66+
redis_db: int = REDIS_DB,
6167
timeout: int = 60,
6268
check_interval: float = 0.1,
6369
clear_buffers: bool = True,
@@ -89,6 +95,7 @@ def __init__(
8995
super().__init__(
9096
models_config_path=models_config_path,
9197
redis_host=redis_host,
98+
redis_password=redis_password,
9299
redis_port=redis_port,
93100
redis_db=redis_db,
94101
timeout=timeout,

llm_router_api/core/lb/strategies/first_available_optim.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import logging
22
from typing import List, Dict, Optional, Any
33

4-
from llm_router_api.base.constants import REDIS_HOST, REDIS_PORT
4+
from llm_router_api.base.constants import (
5+
REDIS_HOST,
6+
REDIS_PORT,
7+
REDIS_PASSWORD,
8+
REDIS_DB,
9+
)
510
from llm_router_api.core.lb.strategies.first_available import FirstAvailableStrategy
611

712

@@ -30,8 +35,9 @@ def __init__(
3035
self,
3136
models_config_path: str,
3237
redis_host: str = REDIS_HOST,
38+
redis_password: str = REDIS_PASSWORD,
3339
redis_port: int = REDIS_PORT,
34-
redis_db: int = 0,
40+
redis_db: int = REDIS_DB,
3541
timeout: int = 60,
3642
check_interval: float = 0.1,
3743
clear_buffers: bool = True,
@@ -40,6 +46,7 @@ def __init__(
4046
super().__init__(
4147
models_config_path=models_config_path,
4248
redis_host=redis_host,
49+
redis_password=redis_password,
4350
redis_port=redis_port,
4451
redis_db=redis_db,
4552
timeout=timeout,

0 commit comments

Comments
 (0)