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
912from __future__ import annotations
1316import re
1417import uuid
1518from 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
1822from config .settings import AppConfig
1923from 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
0 commit comments