Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/pass_through_endpoints/passthrough_guardrails.py: 29%

102 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2Passthrough Guardrails Helper Module 

3 

4Handles guardrail execution for passthrough endpoints with: 

5- Opt-in model (guardrails only run when explicitly configured) 

6- Field-level targeting using JSONPath expressions 

7- Automatic inheritance from org/team/key levels when enabled 

8""" 

9 

10from typing import Any, Final 

11 

12from litellm._logging import verbose_proxy_logger 

13from litellm.proxy._types import ( 

14 PassThroughGuardrailsConfig, 

15 PassThroughGuardrailSettings, 

16 UserAPIKeyAuth, 

17) 

18from litellm.proxy.pass_through_endpoints.jsonpath_extractor import JsonPathExtractor 

19 

20# Type for raw guardrails config input (before normalization) 

21# Can be a list of names or a dict with settings 

22PassThroughGuardrailsConfigInput = ( 

23 list[str] # Simple list: ["guard-1", "guard-2"] 

24 | PassThroughGuardrailsConfig # Dict: {"guard-1": {"request_fields": [...]}} 

25) 

26 

27 

28class PassthroughGuardrailHandler: 

29 """ 

30 Handles guardrail execution for passthrough endpoints. 

31 

32 Passthrough endpoints use an opt-in model for guardrails: 

33 - Guardrails only run when explicitly configured on the endpoint 

34 - Supports field-level targeting using JSONPath expressions 

35 - Automatically inherits org/team/key level guardrails when enabled 

36 

37 Guardrails can be specified as: 

38 - List format (simple): ["guardrail-1", "guardrail-2"] 

39 - Dict format (with settings): {"guardrail-1": {"request_fields": ["query"]}} 

40 """ 

41 

42 @staticmethod 

43 def normalize_config( 

44 guardrails_config: PassThroughGuardrailsConfigInput | None, 

45 ) -> PassThroughGuardrailsConfig | None: 

46 """ 

47 Normalize guardrails config to dict format. 

48 

49 Accepts: 

50 - List of guardrail names: ["g1", "g2"] -> {"g1": None, "g2": None} 

51 - Dict with settings: {"g1": {"request_fields": [...]}} 

52 - None: returns None 

53 """ 

54 if guardrails_config is None: 

55 return None 

56 

57 # Already a dict - return as-is 

58 if isinstance(guardrails_config, dict): 58 ↛ 62line 58 didn't jump to line 62 because the condition on line 58 was always true

59 return guardrails_config 

60 

61 # List of guardrail names - convert to dict 

62 if isinstance(guardrails_config, list): 

63 return {name: None for name in guardrails_config} 

64 

65 verbose_proxy_logger.debug( 

66 "Passthrough guardrails config is not a dict or list, got: %s", 

67 type(guardrails_config), 

68 ) 

69 return None 

70 

71 @staticmethod 

72 def is_enabled( 

73 guardrails_config: PassThroughGuardrailsConfigInput | None, 

74 ) -> bool: 

75 """ 

76 Check if guardrails are enabled for a passthrough endpoint. 

77 

78 Passthrough endpoints are opt-in only - guardrails only run when 

79 the guardrails config is set with at least one guardrail. 

80 """ 

81 normalized: Final = PassthroughGuardrailHandler.normalize_config(guardrails_config) 

82 if normalized is None: 

83 return False 

84 return len(normalized) > 0 

85 

86 @staticmethod 

87 def get_guardrail_names( 

88 guardrails_config: PassThroughGuardrailsConfigInput | None, 

89 ) -> list[str]: 

90 """Get the list of guardrail names configured for a passthrough endpoint.""" 

91 normalized: Final = PassthroughGuardrailHandler.normalize_config(guardrails_config) 

92 if normalized is None: 

93 return [] 

94 return list(normalized.keys()) 

95 

96 @staticmethod 

97 def get_settings( 

98 guardrails_config: PassThroughGuardrailsConfigInput | None, 

99 guardrail_name: str, 

100 ) -> PassThroughGuardrailSettings | None: 

101 """Get settings for a specific guardrail from the passthrough config.""" 

102 normalized: Final = PassthroughGuardrailHandler.normalize_config(guardrails_config) 

103 if normalized is None: 

104 return None 

105 

106 settings: Final = normalized.get(guardrail_name) 

107 if settings is None: 

108 return None 

109 

110 if isinstance(settings, dict): 

111 return PassThroughGuardrailSettings(**settings) 

112 

113 return settings 

114 

115 @staticmethod 

116 def prepare_input( 

117 request_data: dict, 

118 guardrail_settings: PassThroughGuardrailSettings | None, 

119 ) -> str: 

120 """ 

121 Prepare input text for guardrail execution based on field targeting settings. 

122 

123 If request_fields is specified, extracts only those fields. 

124 Otherwise, uses the entire request payload as text. 

125 """ 

126 if guardrail_settings is None or guardrail_settings.request_fields is None: 

127 from litellm.litellm_core_utils.safe_json_dumps import safe_dumps 

128 

129 return safe_dumps(request_data) 

130 

131 return JsonPathExtractor.extract_fields( 

132 data=request_data, 

133 jsonpath_expressions=guardrail_settings.request_fields, 

134 ) 

135 

136 @staticmethod 

137 def prepare_output( 

138 response_data: dict, 

139 guardrail_settings: PassThroughGuardrailSettings | None, 

140 ) -> str: 

141 """ 

142 Prepare output text for guardrail execution based on field targeting settings. 

143 

144 If response_fields is specified, extracts only those fields. 

145 Otherwise, uses the entire response payload as text. 

146 """ 

147 if guardrail_settings is None or guardrail_settings.response_fields is None: 

148 from litellm.litellm_core_utils.safe_json_dumps import safe_dumps 

149 

150 return safe_dumps(response_data) 

151 

152 return JsonPathExtractor.extract_fields( 

153 data=response_data, 

154 jsonpath_expressions=guardrail_settings.response_fields, 

155 ) 

156 

157 @staticmethod 

158 async def execute( 

159 request_data: dict, 

160 user_api_key_dict: UserAPIKeyAuth, 

161 guardrails_config: PassThroughGuardrailsConfig | None, 

162 event_type: str = "pre_call", 

163 ) -> dict: 

164 """ 

165 Execute guardrails for a passthrough endpoint. 

166 

167 This is the main entry point for passthrough guardrail execution. 

168 

169 Args: 

170 request_data: The request payload 

171 user_api_key_dict: User API key authentication info 

172 guardrails_config: Passthrough-specific guardrails configuration 

173 event_type: "pre_call" for request, "post_call" for response 

174 

175 Returns: 

176 The potentially modified request_data 

177 

178 Raises: 

179 HTTPException if a guardrail blocks the request 

180 """ 

181 if not PassthroughGuardrailHandler.is_enabled(guardrails_config): 

182 verbose_proxy_logger.debug("Passthrough guardrails not enabled, skipping guardrail execution") 

183 return request_data 

184 

185 guardrail_names: Final = PassthroughGuardrailHandler.get_guardrail_names(guardrails_config) 

186 verbose_proxy_logger.debug("Executing passthrough guardrails: %s", guardrail_names) 

187 

188 # Add to request metadata so guardrails know which to run 

189 from litellm.proxy.pass_through_endpoints.passthrough_context import ( 

190 set_passthrough_guardrails_config, 

191 ) 

192 

193 if "metadata" not in request_data: 

194 request_data["metadata"] = {} 

195 

196 # Set guardrails in metadata using dict format for compatibility 

197 request_data["metadata"]["guardrails"] = {name: True for name in guardrail_names} 

198 

199 # Store passthrough guardrails config in request-scoped context 

200 set_passthrough_guardrails_config(guardrails_config) 

201 

202 return request_data 

203 

204 @staticmethod 

205 def collect_guardrails( 

206 user_api_key_dict: UserAPIKeyAuth, 

207 passthrough_guardrails_config: PassThroughGuardrailsConfigInput | None, 

208 ) -> dict[str, bool] | None: 

209 """ 

210 Collect guardrails for a passthrough endpoint. 

211 

212 Passthrough endpoints are opt-in only for guardrails. Guardrails only run when 

213 the guardrails config is set with at least one guardrail. 

214 

215 Accepts both list and dict formats: 

216 - List: ["guardrail-1", "guardrail-2"] 

217 - Dict: {"guardrail-1": {"request_fields": [...]}} 

218 

219 When enabled, this function collects: 

220 - Passthrough-specific guardrails from the config 

221 - Org/team/key level guardrails (automatic inheritance when passthrough is enabled) 

222 

223 Args: 

224 user_api_key_dict: User API key authentication info 

225 passthrough_guardrails_config: List or Dict of guardrail names/settings 

226 

227 Returns: 

228 Dict of guardrail names to run (format: {guardrail_name: True}), or None 

229 """ 

230 from litellm.proxy.litellm_pre_call_utils import ( 

231 _add_guardrails_from_key_or_team_metadata, 

232 ) 

233 

234 # Normalize config to dict format (handles both list and dict) 

235 normalized_config: Final = PassthroughGuardrailHandler.normalize_config(passthrough_guardrails_config) 

236 

237 if normalized_config is None: 

238 verbose_proxy_logger.debug("Passthrough guardrails not configured, skipping guardrail collection") 

239 return None 

240 

241 if len(normalized_config) == 0: 241 ↛ 246line 241 didn't jump to line 246 because the condition on line 241 was always true

242 verbose_proxy_logger.debug("Passthrough guardrails config is empty, skipping") 

243 return None 

244 

245 # Passthrough is enabled - collect guardrails 

246 guardrails_to_run: Final[dict[str, bool]] = {} 

247 

248 # Add passthrough-specific guardrails 

249 for guardrail_name in normalized_config: 

250 guardrails_to_run[guardrail_name] = True 

251 verbose_proxy_logger.debug("Added passthrough-specific guardrail: %s", guardrail_name) 

252 

253 # Add org/team/key level guardrails using shared helper 

254 temp_data: Final[dict[str, Any]] = {"metadata": {}} 

255 _add_guardrails_from_key_or_team_metadata( 

256 key_metadata=user_api_key_dict.metadata, 

257 team_metadata=user_api_key_dict.team_metadata, 

258 data=temp_data, 

259 metadata_variable_name="metadata", 

260 ) 

261 

262 # Merge inherited guardrails into guardrails_to_run 

263 inherited_guardrails: Final = temp_data["metadata"].get("guardrails", []) 

264 for guardrail_name in inherited_guardrails: 

265 if guardrail_name not in guardrails_to_run: 

266 guardrails_to_run[guardrail_name] = True 

267 verbose_proxy_logger.debug("Added inherited guardrail (key/team level): %s", guardrail_name) 

268 

269 verbose_proxy_logger.debug( 

270 "Collected guardrails for passthrough endpoint: %s", 

271 list(guardrails_to_run.keys()), 

272 ) 

273 

274 return guardrails_to_run if guardrails_to_run else None 

275 

276 @staticmethod 

277 def get_field_targeted_text( 

278 data: dict, 

279 guardrail_name: str, 

280 is_request: bool = True, 

281 ) -> str | None: 

282 """ 

283 Get the text to check for a guardrail, respecting field targeting settings. 

284 

285 Called by guardrail hooks to get the appropriate text based on 

286 passthrough field targeting configuration. 

287 

288 Args: 

289 data: The request/response data dict 

290 guardrail_name: Name of the guardrail being executed 

291 is_request: True for request (pre_call), False for response (post_call) 

292 

293 Returns: 

294 The text to check, or None to use default behavior 

295 """ 

296 from litellm.proxy.pass_through_endpoints.passthrough_context import ( 

297 get_passthrough_guardrails_config, 

298 ) 

299 

300 passthrough_config: Final = get_passthrough_guardrails_config() 

301 if passthrough_config is None: 

302 return None 

303 

304 settings: Final = PassthroughGuardrailHandler.get_settings(passthrough_config, guardrail_name) 

305 if settings is None: 

306 return None 

307 

308 if is_request: 

309 if settings.request_fields: 

310 return JsonPathExtractor.extract_fields(data, settings.request_fields) 

311 else: 

312 if settings.response_fields: 

313 return JsonPathExtractor.extract_fields(data, settings.response_fields) 

314 

315 return None