Skip to content

Commit 14b7c2f

Browse files
author
Wojciech Napierała
committed
Enhance LiteLLM integration with debug toggling and provider overrides
- Added configuration options for enabling LiteLLM debugging in README.md and settings.py. - Implemented provider-specific model routing and credential handling in review.py and task_executor.py. - Updated automated review functionality to respect the LiteLLM debug setting and allow for provider overrides in review configurations. - Enhanced test coverage for model resolution and JSON serialization in test_review_engine.py.
1 parent 572f048 commit 14b7c2f

5 files changed

Lines changed: 220 additions & 13 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ The project targets **Python 3.12**. Create a virtual environment with that inte
4949
and optionally `AZURE_OPENAI_API_VERSION`.
5050
- Ollama: override `OLLAMA_BASE_URL` when running a remote instance.
5151
- Missing credentials cause a descriptive `WorkflowError` to surface.
52+
- Enable LiteLLM debugging by setting `llm.enable_debug` to `true` in the
53+
configuration when deeper request/response tracing is needed.
54+
- Automated review can use a distinct provider by setting
55+
`review.auto_reviewer_provider`; if omitted it inherits the default workflow's
56+
provider.
5257
- Embeddings (default: Azure OpenAI `text-embedding-3-large`):
5358
- Set `AZURE_OPENAI_EMBEDDING_DEPLOYMENT` if your embedding deployment name
5459
differs from the model identifier.

config/settings.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Helpers for loading and validating DRM configuration files.
22
3-
Updates: v0.1 - 2025-11-06 - Added Pydantic-based loader for core configuration.
3+
Updates:
4+
v0.1 - 2025-11-06 - Added Pydantic-based loader for core configuration.
5+
v0.2 - 2025-11-07 - Added LiteLLM debug toggle to LLM configuration schema.
6+
v0.3 - 2025-11-07 - Added review model provider overrides.
47
"""
58

69
from __future__ import annotations
@@ -44,6 +47,10 @@ class LLMConfig(BaseModel):
4447
default_workflow: str = Field(..., min_length=1)
4548
workflows: Dict[str, WorkflowModelConfig]
4649
timeouts: WorkflowTimeoutConfig
50+
enable_debug: bool = Field(
51+
False,
52+
description="Turn on LiteLLM's verbose debug logging across workflows.",
53+
)
4754

4855
@model_validator(mode="after")
4956
def _ensure_default_present(self) -> "LLMConfig":
@@ -86,6 +93,13 @@ class ReviewConfig(BaseModel):
8693
default=None,
8794
description="Model identifier used for automated audits.",
8895
)
96+
auto_reviewer_provider: Optional[str] = Field(
97+
default=None,
98+
description=(
99+
"Optional provider override for the automated review model; defaults "
100+
"to the application's default workflow provider."
101+
),
102+
)
89103

90104

91105
class TelemetryConfig(BaseModel):

core/review.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
v0.1 - 2025-11-06 - Added ReviewEngine with optional LiteLLM-based automated audits and structured review records.
55
v0.2 - 2025-11-06 - Expanded automated review rubric and context framing.
66
v0.3 - 2025-11-07 - Parsed automated review verdict, score, and suggestions into structured fields.
7+
v0.4 - 2025-11-07 - Honoured LiteLLM debug toggle for automated reviews.
8+
v0.5 - 2025-11-07 - Normalised metadata serialisation for automated review payloads.
9+
v0.6 - 2025-11-07 - Applied provider-aware routing for automated review models.
710
"""
811

912
from __future__ import annotations
@@ -13,7 +16,8 @@
1316
import re
1417
import uuid
1518
from dataclasses import dataclass
16-
from typing import Any, List, Optional, Sequence, Tuple, cast
19+
from os import getenv
20+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, cast
1721

1822
from config.settings import AppConfig
1923
from core.exceptions import ReviewError
@@ -45,6 +49,7 @@ class ReviewEngine:
4549
def __init__(self, config: AppConfig) -> None:
4650
self._config = config
4751
self._logger = LOGGER
52+
self._activate_litellm_debug()
4853

4954
def perform_review(
5055
self,
@@ -106,19 +111,21 @@ def _run_automated_review(
106111

107112
try:
108113
self._logger.debug("Running automated review with model %s", model)
114+
model_name, provider_kwargs = self._resolve_model_configuration()
115+
payload = {
116+
"task_prompt": request.prompt,
117+
"workflow": request.workflow,
118+
"context": self._to_json_safe(request.context),
119+
"result": result.content,
120+
"metadata": self._to_json_safe(result.metadata),
121+
}
109122
review_payload = json.dumps(
110-
{
111-
"task_prompt": request.prompt,
112-
"workflow": request.workflow,
113-
"context": request.context,
114-
"result": result.content,
115-
"metadata": result.metadata,
116-
},
123+
payload,
117124
ensure_ascii=False,
118125
indent=2,
119126
)
120127
response = litellm.completion(
121-
model=model,
128+
model=model_name,
122129
messages=[
123130
{"role": "system", "content": REVIEW_SYSTEM_PROMPT},
124131
{
@@ -132,6 +139,7 @@ def _run_automated_review(
132139
],
133140
temperature=0.0,
134141
request_timeout=self._config.llm.timeouts.request_seconds,
142+
**provider_kwargs,
135143
)
136144
auto_notes = response["choices"][0]["message"]["content"]
137145
parsed = self._parse_automated_review(auto_notes)
@@ -178,6 +186,88 @@ def _normalise_verdict(raw_verdict: Optional[str]) -> str:
178186
return "fail-auto"
179187
return verdict
180188

189+
def _activate_litellm_debug(self) -> None:
190+
"""Enable LiteLLM debug logging for automated review when configured."""
191+
if not self._config.llm.enable_debug:
192+
return
193+
194+
if litellm is None:
195+
self._logger.warning(
196+
"LiteLLM debug requested for reviews but the library is not installed."
197+
)
198+
return
199+
200+
debug_hook = getattr(litellm, "_turn_on_debug", None)
201+
if callable(debug_hook):
202+
debug_hook()
203+
self._logger.info("LiteLLM debug logging enabled for review engine.")
204+
else:
205+
self._logger.warning(
206+
"LiteLLM debug requested but '_turn_on_debug' is unavailable on the library."
207+
)
208+
209+
@staticmethod
210+
def _to_json_safe(value: Any) -> Any:
211+
"""Convert the value into JSON-serialisable primitives."""
212+
if value is None or isinstance(value, (str, int, float, bool)):
213+
return value
214+
215+
if isinstance(value, Mapping):
216+
return {str(key): ReviewEngine._to_json_safe(item) for key, item in value.items()}
217+
218+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
219+
return [ReviewEngine._to_json_safe(item) for item in value]
220+
221+
model_dump = getattr(value, "model_dump", None)
222+
if callable(model_dump):
223+
return ReviewEngine._to_json_safe(model_dump())
224+
225+
if hasattr(value, "__dict__"):
226+
return ReviewEngine._to_json_safe(vars(value))
227+
228+
return str(value)
229+
230+
def _resolve_model_configuration(self) -> Tuple[str, Dict[str, object]]:
231+
"""Return the provider-aware model identifier and kwargs for LiteLLM."""
232+
model_name = self._config.review.auto_reviewer_model
233+
if not model_name:
234+
raise ReviewError("Automated review model is not configured.")
235+
236+
provider = self._config.review.auto_reviewer_provider
237+
if not provider:
238+
default = self._config.llm.default_workflow
239+
default_cfg = self._config.llm.workflows.get(default)
240+
provider = default_cfg.provider if default_cfg else None
241+
242+
if not provider:
243+
return model_name, {}
244+
245+
provider_lower = provider.lower()
246+
if provider_lower == "azure":
247+
api_key = getenv("AZURE_OPENAI_API_KEY")
248+
endpoint = getenv("AZURE_OPENAI_ENDPOINT")
249+
api_version = getenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview")
250+
if not api_key or not endpoint:
251+
raise ReviewError(
252+
"Azure OpenAI credentials missing for automated review."
253+
)
254+
base = endpoint.rstrip("/")
255+
if not model_name.startswith("azure/"):
256+
model_name = f"azure/{model_name}"
257+
return model_name, {
258+
"api_key": api_key,
259+
"api_base": base,
260+
"base_url": base,
261+
"api_version": api_version,
262+
"custom_llm_provider": "azure",
263+
}
264+
265+
if provider_lower == "ollama":
266+
base_url = getenv("OLLAMA_BASE_URL", "http://localhost:11434")
267+
return model_name, {"base_url": base_url.rstrip("/")}
268+
269+
return model_name, {}
270+
181271
def _parse_automated_review(self, content: str) -> "AutomatedReview":
182272
lines = [line.rstrip() for line in content.splitlines()]
183273
verdict: Optional[str] = None

core/task_executor.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
scaffold with retries and telemetry hooks.
66
v0.2 - 2025-11-06 - Wired provider credential handling for Azure and Ollama.
77
v0.3 - 2025-11-06 - Integrated controller bias into workflow selection metadata.
8+
v0.4 - 2025-11-07 - Enabled optional LiteLLM debug toggling from configuration.
9+
v0.5 - 2025-11-07 - Normalised Azure provider routing for LiteLLM compatibility.
810
"""
911

1012
from __future__ import annotations
@@ -14,7 +16,7 @@
1416
from os import getenv
1517
from typing import Any, Dict, Mapping, Optional, Tuple, cast
1618

17-
from config.settings import AppConfig
19+
from config.settings import AppConfig, WorkflowModelConfig
1820
from core.controller import SelfAdjustingController
1921
from core.exceptions import WorkflowError
2022
from models.workflows import TaskRequest, TaskResult, WorkflowSelection
@@ -40,6 +42,7 @@ def __init__(
4042
self._config = config
4143
self._logger = LOGGER
4244
self._controller = controller
45+
self._activate_litellm_debug()
4346

4447
def select_workflow(self, requested: Optional[str] = None) -> WorkflowSelection:
4548
"""Select the best workflow given request metadata."""
@@ -124,6 +127,7 @@ def execute(self, request: TaskRequest) -> TaskResult:
124127
workflow_cfg = workflows[request.workflow]
125128
timeout_cfg = self._config.llm.timeouts
126129
provider_kwargs = self._build_provider_kwargs(workflow_cfg.provider)
130+
model_identifier = self._resolve_model_name(workflow_cfg)
127131

128132
attempt = 0
129133
delay = timeout_cfg.retry_backoff_seconds
@@ -136,7 +140,7 @@ def execute(self, request: TaskRequest) -> TaskResult:
136140
"Executing workflow '%s' attempt %s", request.workflow, attempt
137141
)
138142
response = litellm.completion(
139-
model=workflow_cfg.model,
143+
model=model_identifier,
140144
messages=[
141145
{"role": "system", "content": request.context.get("system", "")},
142146
{"role": "user", "content": request.prompt},
@@ -195,10 +199,13 @@ def _build_provider_kwargs(self, provider: str) -> Dict[str, object]:
195199
"Azure OpenAI credentials missing. Set AZURE_OPENAI_API_KEY and "
196200
"AZURE_OPENAI_ENDPOINT environment variables."
197201
)
202+
base = endpoint.rstrip("/")
198203
return {
199204
"api_key": api_key,
200-
"base_url": endpoint.rstrip("/"),
205+
"api_base": base,
206+
"base_url": base,
201207
"api_version": api_version,
208+
"custom_llm_provider": "azure",
202209
}
203210

204211
if provider.lower() == "ollama":
@@ -217,3 +224,34 @@ def _redact_sensitive(payload: Dict[str, object]) -> Dict[str, object]:
217224
else:
218225
redacted[key] = value
219226
return redacted
227+
228+
def _activate_litellm_debug(self) -> None:
229+
"""Enable LiteLLM debug logging when requested via configuration."""
230+
if not self._config.llm.enable_debug:
231+
return
232+
233+
if litellm is None:
234+
self._logger.warning(
235+
"LiteLLM debug requested but the library is not installed."
236+
)
237+
return
238+
239+
debug_hook = getattr(litellm, "_turn_on_debug", None)
240+
if callable(debug_hook):
241+
debug_hook()
242+
self._logger.info("LiteLLM debug logging enabled.")
243+
else:
244+
self._logger.warning(
245+
"LiteLLM debug requested but '_turn_on_debug' is unavailable on the library."
246+
)
247+
248+
def _resolve_model_name(self, workflow_cfg: WorkflowModelConfig) -> str:
249+
"""Normalise provider-specific model identifiers for LiteLLM."""
250+
model_name = workflow_cfg.model
251+
if workflow_cfg.provider.lower() != "azure":
252+
return model_name
253+
254+
if model_name.startswith("azure/"):
255+
return model_name
256+
257+
return f"azure/{model_name}"

tests/test_review_engine.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_parse_automated_review_structured_fields(tmp_path: Path) -> None:
5555
def test_live_task_loop_persists_artifacts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
5656
config = _load_sample_config(tmp_path)
5757
config.review.auto_reviewer_model = "review-stub"
58+
config.review.auto_reviewer_provider = "ollama"
5859
config.llm.workflows["fast"].provider = "ollama"
5960
config.llm.workflows["fast"].model = "stub-fast-model"
6061

@@ -126,3 +127,62 @@ class DummyTimeout(Exception):
126127

127128
working_items = loop._memory_manager.list_working_items()
128129
assert any(item.key.endswith(":result") for item in working_items)
130+
131+
132+
def test_to_json_safe_serialises_usage_objects() -> None:
133+
payload = {
134+
"usage": SimpleNamespace(total_tokens=42, prompt_tokens=10),
135+
"sequence": [SimpleNamespace(value="a")],
136+
"primitive": "ok",
137+
}
138+
safe = ReviewEngine._to_json_safe(payload)
139+
assert safe == {
140+
"usage": {"total_tokens": 42, "prompt_tokens": 10},
141+
"sequence": [{"value": "a"}],
142+
"primitive": "ok",
143+
}
144+
145+
146+
def test_resolve_model_configuration_uses_azure_provider(monkeypatch: pytest.MonkeyPatch) -> None:
147+
config = settings.AppConfig.model_validate(
148+
{
149+
"version": "0.1",
150+
"llm": {
151+
"default_workflow": "fast",
152+
"workflows": {
153+
"fast": {
154+
"provider": "azure",
155+
"model": "gpt-4.1",
156+
"temperature": 0.2,
157+
}
158+
},
159+
"timeouts": {
160+
"request_seconds": 10,
161+
"retry_attempts": 1,
162+
"retry_backoff_seconds": 1,
163+
},
164+
"enable_debug": False,
165+
},
166+
"memory": {
167+
"redis": {"host": "localhost", "port": 6379, "db": 0, "ttl_seconds": 120},
168+
"chromadb": {"persist_directory": "data/chromadb", "collection": "test"},
169+
},
170+
"review": {
171+
"enabled": True,
172+
"auto_reviewer_model": "gpt-4.1",
173+
"auto_reviewer_provider": None,
174+
},
175+
"embedding": None,
176+
"telemetry": {"log_level": "INFO"},
177+
}
178+
)
179+
180+
monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-key")
181+
monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com")
182+
monkeypatch.setenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview")
183+
184+
engine = ReviewEngine(config)
185+
model_name, kwargs = engine._resolve_model_configuration()
186+
assert model_name == "azure/gpt-4.1"
187+
assert kwargs["custom_llm_provider"] == "azure"
188+
assert kwargs["api_base"] == "https://example.openai.azure.com"

0 commit comments

Comments
 (0)