Skip to content

Commit 8f54bda

Browse files
authored
Merge pull request lightspeed-core#200 from asamal4/dynamic-llm-param
[LEADS-211] feat: ability to add/remove model parameters from config
2 parents 00b2953 + 801b169 commit 8f54bda

13 files changed

Lines changed: 321 additions & 118 deletions

File tree

docs/configuration.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,31 @@ Define a centralized pool of LLM configurations for the Judge Panel feature.
2626
| Setting | Description |
2727
|---------|-------------|
2828
| `llm_pool.defaults.cache_dir` | Cache directory (default: `.caches/llm_cache`) |
29-
| `llm_pool.defaults.timeout` | Request timeout in seconds |
30-
| `llm_pool.defaults.num_retries` | Retry attempts |
29+
| `llm_pool.defaults.timeout` | Request timeout in seconds (default: `300`) |
30+
| `llm_pool.defaults.num_retries` | Retry attempts (default: `3`) |
3131
| `llm_pool.defaults.parameters.temperature` | Sampling temperature |
3232
| `llm_pool.defaults.parameters.max_completion_tokens` | Max tokens in response |
33+
| `llm_pool.defaults.parameters.*` | Any additional provider-supported LLM parameter (e.g., `top_p`, `frequency_penalty`) |
3334
| `llm_pool.models.<id>.provider` | LLM provider (required) |
3435
| `llm_pool.models.<id>.model` | Model name |
35-
| `llm_pool.models.<id>.parameters.*` | Model-specific parameter overrides |
36+
| `llm_pool.models.<id>.parameters.*` | Model-specific parameter overrides (merged with defaults, model takes priority) |
37+
38+
**Dynamic Parameters:** The `parameters` dict accepts any key-value pair supported by the LLM provider. Known parameters: `temperature`, `max_completion_tokens`. Unsupported parameters are silently dropped by the provider.
39+
40+
**Removing Defaults:** To remove an inherited default parameter, set it to `null` in the model config:
41+
```yaml
42+
models:
43+
no-temp-model:
44+
provider: openai
45+
parameters:
46+
temperature: null # Removes default temperature, won't be sent to API
47+
```
3648

3749
```yaml
3850
llm_pool:
3951
defaults:
4052
cache_dir: ".caches/llm_cache"
53+
num_retries: 2
4154
parameters:
4255
temperature: 0.0
4356
max_completion_tokens: 512
@@ -48,6 +61,8 @@ llm_pool:
4861
judge-4.1-mini:
4962
provider: openai
5063
model: gpt-4.1-mini
64+
parameters:
65+
temperature: 0.3 # Overrides default
5166
```
5267

5368
## Judge Panel
@@ -81,8 +96,11 @@ judge_panel:
8196

8297
---
8398

84-
## Judge LLM configuration
85-
This section configures LLM as a judge for both Ragas and DeepEval.
99+
## Judge LLM configuration (legacy)
100+
101+
> **Deprecation notice:** The `llm` section will be replaced by `llm_pool` + `judge_panel`.
102+
103+
This section configures LLM. It is used when `judge_panel` is not configured (even if `llm_pool` exists).
86104

87105
### LLM
88106
| Setting (llm.) | Default | Description |
@@ -98,6 +116,8 @@ This section configures LLM as a judge for both Ragas and DeepEval.
98116
| cache_dir | `".caches/llm_cache"` | Directory with cached LLM responses |
99117
| cache_enabled | `true` | Is LLM cache enabled? |
100118

119+
Dynamic LLM parameters are only supported through `llm_pool` config. To use dynamic parameters, migrate to `llm_pool`.
120+
101121
**Note**: For RHAIIS, models.corp, or other vLLM-based inference servers, use the `hosted_vllm` provider configuration. `models.corp` additionally requires certificate setup via `ssl_cert_file` configuration option.
102122

103123
### Embeddings

src/lightspeed_evaluation/core/llm/custom.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Base Custom LLM class for evaluation framework."""
22

33
import logging
4-
from typing import Any, Optional, Union
4+
from typing import Any, Union
55

66
import litellm
77
from litellm.exceptions import InternalServerError
@@ -16,7 +16,13 @@ class BaseCustomLLM: # pylint: disable=too-few-public-methods
1616
"""Base LLM class with core calling functionality."""
1717

1818
def __init__(self, model_name: str, llm_params: dict[str, Any]):
19-
"""Initialize with model configuration."""
19+
"""Initialize with model configuration.
20+
21+
Args:
22+
model_name: Constructed model name (e.g., "openai/gpt-4")
23+
llm_params: LLM params from get_llm_params() — operational fields
24+
plus a "parameters" dict with inference params.
25+
"""
2026
self.model_name = model_name
2127
self.llm_params = llm_params
2228

@@ -29,7 +35,6 @@ def call(
2935
self,
3036
prompt: str,
3137
n: int = 1,
32-
temperature: Optional[float] = None,
3338
return_single: bool = True,
3439
**kwargs: Any,
3540
) -> Union[str, list[str]]:
@@ -38,27 +43,20 @@ def call(
3843
Args:
3944
prompt: Text prompt to send
4045
n: Number of responses to generate (default 1)
41-
temperature: Override temperature (uses config default if None)
4246
return_single: If True and n=1, return single string. If False, always return list.
4347
**kwargs: Additional LLM parameters
4448
4549
Returns:
4650
Single string if return_single=True and n=1, otherwise list of strings
4751
"""
48-
temp = (
49-
temperature
50-
if temperature is not None
51-
else self.llm_params.get("temperature", 0.0)
52-
)
53-
52+
# Note: Forbidden keys are rejected at LLMParametersConfig load time
5453
call_params = {
5554
"model": self.model_name,
5655
"messages": [{"role": "user", "content": prompt}],
57-
"temperature": temp,
5856
"n": n,
59-
"max_completion_tokens": self.llm_params.get("max_completion_tokens"),
6057
"timeout": self.llm_params.get("timeout"),
61-
"num_retries": self.llm_params.get("num_retries", 3),
58+
"num_retries": self.llm_params.get("num_retries"),
59+
**self.llm_params.get("parameters", {}),
6260
**kwargs,
6361
}
6462

src/lightspeed_evaluation/core/llm/deepeval.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ class DeepEvalLLMManager:
2323
"""
2424

2525
def __init__(self, model_name: str, llm_params: dict[str, Any]):
26-
"""Initialize with LLM parameters from LLMManager."""
26+
"""Initialize with LLM parameters from LLMManager.
27+
28+
Args:
29+
model_name: Constructed model name (e.g., "openai/gpt-4")
30+
llm_params: LLM params from get_llm_params() — operational fields
31+
plus a "parameters" dict with inference params.
32+
"""
2733
self.model_name = model_name
2834
self.llm_params = llm_params
2935

@@ -34,14 +40,16 @@ def __init__(self, model_name: str, llm_params: dict[str, Any]):
3440

3541
# Note: Token tracking is handled by the patched litellm.completion/acompletion
3642
# No additional setup needed - the patch was applied at module import time
37-
3843
# Create standard LiteLLMModel - it will use our patched completion functions
44+
45+
# LiteLLMModel stores **kwargs in self.kwargs and merges them into
46+
# every litellm.completion() call
47+
# Note: Forbidden keys are rejected at LLMParametersConfig load time
3948
self.llm_model = LiteLLMModel(
4049
model=self.model_name,
41-
temperature=llm_params.get("temperature", 0.0),
42-
max_completion_tokens=llm_params.get("max_completion_tokens"),
43-
timeout=llm_params.get("timeout"),
44-
num_retries=llm_params.get("num_retries", 3),
50+
timeout=self.llm_params.get("timeout"),
51+
num_retries=self.llm_params.get("num_retries"),
52+
**self.llm_params.get("parameters", {}),
4553
)
4654

4755
print(f"✅ DeepEval LLM Manager: {self.model_name}")
@@ -65,13 +73,12 @@ def get_llm(self) -> LiteLLMModel:
6573

6674
def get_model_info(self) -> dict[str, Any]:
6775
"""Get information about the configured model."""
68-
return {
69-
"model_name": self.model_name,
70-
"temperature": self.llm_params.get("temperature", 0.0),
71-
"max_completion_tokens": self.llm_params.get("max_completion_tokens"),
72-
"timeout": self.llm_params.get("timeout"),
73-
"num_retries": self.llm_params.get("num_retries", 3),
74-
}
76+
info: dict[str, Any] = {"model_name": self.model_name}
77+
info["timeout"] = self.llm_params.get("timeout")
78+
info["num_retries"] = self.llm_params.get("num_retries")
79+
# Add inference params (forbidden keys rejected at config load time)
80+
info.update(self.llm_params.get("parameters", {}))
81+
return info
7582

7683
@staticmethod
7784
def flush_deepevals_pending_tasks() -> None:

src/lightspeed_evaluation/core/llm/manager.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,18 @@ def get_model_name(self) -> str:
209209
return self.model_name
210210

211211
def get_llm_params(self) -> dict[str, Any]:
212-
"""Get parameters for LLM completion calls."""
212+
"""Get all LLM parameters for adapter consumption.
213+
214+
Returns operational params and inference parameters dict.
215+
Adapters read operational fields directly and spread **parameters
216+
for inference params (temperature, max_completion_tokens, etc.).
217+
"""
213218
return {
214219
"model": self.model_name,
215-
"temperature": self.config.temperature,
216-
# Map max_tokens to max_completion_tokens for LLM API
217-
"max_completion_tokens": self.config.max_tokens,
218220
"timeout": self.config.timeout,
219221
"num_retries": self.config.num_retries,
220222
"ssl_verify": self.config.ssl_verify,
223+
"parameters": dict(self.config.parameters),
221224
}
222225

223226
def get_config(self) -> LLMConfig:

src/lightspeed_evaluation/core/llm/ragas.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,24 @@ def __init__(self, llm_manager: LLMManager):
4242
# Setup SSL verification for litellm
4343
setup_litellm_ssl(self.llm_params)
4444

45-
# Extract inference kwargs to pass to ragas llm_factory
46-
# Filter out None values to avoid overriding library defaults unnecessarily
47-
inference_kwargs: dict[str, Any] = {}
48-
for key in ("temperature", "timeout", "num_retries"):
49-
if self.llm_params.get(key) is not None:
50-
inference_kwargs[key] = self.llm_params[key]
51-
45+
# Build inference kwargs from parameters
5246
# Rename max_completion_tokens to max_tokens for ragas/instructor compatibility
5347
# (OpenAI rejects requests with both set simultaneously)
54-
if self.llm_params.get("max_completion_tokens") is not None:
55-
inference_kwargs["max_tokens"] = self.llm_params["max_completion_tokens"]
48+
# Note: Forbidden keys are rejected at LLMParametersConfig load time
49+
inference_kwargs = dict(self.llm_params.get("parameters", {}))
50+
if "max_completion_tokens" in inference_kwargs:
51+
inference_kwargs["max_tokens"] = inference_kwargs.pop(
52+
"max_completion_tokens"
53+
)
5654

5755
# Create LLM using ragas llm_factory with litellm provider
5856
# MUST use acompletion (async) because ragas 0.4 metrics use async internally
5957
self.llm = llm_factory(
60-
model=self.model_name,
6158
provider="litellm",
6259
client=litellm.acompletion,
60+
model=self.model_name,
61+
timeout=self.llm_params.get("timeout"),
62+
num_retries=self.llm_params.get("num_retries"),
6363
**inference_kwargs,
6464
)
6565

@@ -75,7 +75,9 @@ def get_llm(self) -> Any:
7575

7676
def get_model_info(self) -> dict[str, Any]:
7777
"""Get information about the configured model."""
78-
return {
79-
"model_name": self.model_name,
80-
"temperature": self.llm_params.get("temperature", 0.0),
81-
}
78+
params = self.llm_params.get("parameters", {})
79+
info: dict[str, Any] = {"model_name": self.model_name}
80+
# Only include temperature if explicitly set (not removed via null)
81+
if "temperature" in params:
82+
info["temperature"] = params["temperature"]
83+
return info

src/lightspeed_evaluation/core/models/system.py

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@
4646
)
4747
from lightspeed_evaluation.core.system.exceptions import ConfigurationError
4848

49+
# Keys not allowed in llm_pool.parameters
50+
# These are either request-envelope fields or managed via dedicated config fields
51+
FORBIDDEN_PARAMETER_KEYS = frozenset(
52+
{
53+
# Request envelope fields
54+
"model",
55+
"messages",
56+
"n",
57+
# Operational fields (set via dedicated config)
58+
"timeout",
59+
"num_retries",
60+
"ssl_verify",
61+
"ssl_cert_file",
62+
"cache_enabled",
63+
"cache_dir",
64+
# Provider/client fields
65+
"provider",
66+
"client",
67+
}
68+
)
69+
4970

5071
class LLMConfig(BaseModel):
5172
"""LLM configuration from system configuration."""
@@ -119,6 +140,35 @@ def validate_ssl_cert_file(self) -> "LLMConfig":
119140
cache_enabled: bool = Field(
120141
default=True, description="Is caching of 'LLM as a judge' queries enabled?"
121142
)
143+
parameters: dict[str, Any] = Field(
144+
default_factory=dict,
145+
description="Internal: dynamic LLM parameters for API calls",
146+
)
147+
148+
@model_validator(mode="before")
149+
@classmethod
150+
def strip_user_parameters(cls, data: Any) -> Any:
151+
"""Strip parameters from YAML/dict input.
152+
153+
Dynamic parameters via YAML are only supported through llm_pool.
154+
For legacy llm:, parameters is always built from explicit fields.
155+
"""
156+
if isinstance(data, dict):
157+
data.pop("parameters", None)
158+
return data
159+
160+
@model_validator(mode="after")
161+
def build_parameters_from_fields(self) -> "LLMConfig":
162+
"""Build parameters dict from explicit temperature and max_tokens.
163+
164+
For the pool path, resolve_llm_config() overrides this via
165+
model_copy(update=...) after construction.
166+
"""
167+
self.parameters = {
168+
"temperature": self.temperature,
169+
"max_completion_tokens": self.max_tokens,
170+
}
171+
return self
122172

123173

124174
class EmbeddingConfig(BaseModel):
@@ -410,19 +460,37 @@ class LLMParametersConfig(BaseModel):
410460
description="Maximum tokens in response",
411461
)
412462

463+
@model_validator(mode="before")
464+
@classmethod
465+
def reject_forbidden_keys(cls, data: Any) -> Any:
466+
"""Reject keys that must be set via dedicated config fields, not parameters."""
467+
if not isinstance(data, dict):
468+
return data
469+
forbidden_found = FORBIDDEN_PARAMETER_KEYS & data.keys()
470+
if forbidden_found:
471+
raise ConfigurationError(
472+
f"Keys not allowed in parameters: {forbidden_found}. "
473+
f"Use dedicated config fields instead."
474+
)
475+
return data
476+
413477
def to_dict(self, exclude_none: bool = True) -> dict[str, Any]:
414478
"""Convert parameters to dict for passing to LLM.
415479
416480
Args:
417-
exclude_none: If True, exclude None values from output
481+
exclude_none: If True, exclude None values from output.
482+
If False, include only explicitly set fields (uses model_fields_set
483+
to distinguish user-provided None from unset defaults).
418484
419485
Returns:
420486
Dict of parameters ready for LLM API call
421487
"""
422-
params = self.model_dump()
423488
if exclude_none:
424-
return {k: v for k, v in params.items() if v is not None}
425-
return params
489+
return {k: v for k, v in self.model_dump().items() if v is not None}
490+
# Include only fields the user explicitly set (including None overrides)
491+
return {
492+
k: v for k, v in self.model_dump().items() if k in self.model_fields_set
493+
}
426494

427495

428496
class LLMDefaultsConfig(BaseModel):
@@ -581,16 +649,19 @@ def resolve_llm_config(
581649
)
582650
entry = self.models[model_id]
583651

584-
# Merge parameters: defaults -> model entry
652+
# Merge parameters: defaults -> individual (individual overrides defaults).
653+
# None in individual explicitly removes the default's value.
654+
# Note: Forbidden keys are rejected at LLMParametersConfig load time.
585655
merged_params: dict[str, Any] = {}
586656
merged_params.update(self.defaults.parameters.to_dict(exclude_none=True))
587-
merged_params.update(entry.parameters.to_dict(exclude_none=True))
657+
merged_params.update(entry.parameters.to_dict(exclude_none=False))
658+
merged_params = {k: v for k, v in merged_params.items() if v is not None}
588659

589660
# Build cache_dir from defaults with model-specific suffix
590661
suffix = cache_suffix if cache_suffix else model_id
591662
cache_dir = os.path.join(self.defaults.cache_dir, suffix)
592663

593-
return LLMConfig(
664+
config = LLMConfig(
594665
provider=entry.provider,
595666
model=entry.model or model_id,
596667
temperature=merged_params.get("temperature", DEFAULT_LLM_TEMPERATURE),
@@ -609,6 +680,7 @@ def resolve_llm_config(
609680
cache_dir=cache_dir,
610681
# Note: api_base and api_key_path are not propagated yet - requires LLMConfig extension
611682
)
683+
return config.model_copy(update={"parameters": merged_params})
612684

613685

614686
class JudgePanelConfig(BaseModel):

0 commit comments

Comments
 (0)