Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/litellm_pre_call_utils.py: 46%

1376 statements  

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

1import asyncio 

2import copy 

3import json 

4import re 

5import time 

6from collections import OrderedDict 

7from collections.abc import Mapping, MutableMapping, Sequence 

8from datetime import datetime 

9from types import MappingProxyType 

10from typing import TYPE_CHECKING, Any, Final, cast 

11 

12from fastapi import HTTPException, Request 

13from pydantic import TypeAdapter 

14from pydantic import ValidationError as PydanticValidationError 

15from starlette.datastructures import Headers 

16 

17import litellm 

18from litellm._logging import verbose_logger, verbose_proxy_logger 

19from litellm._service_logger import ServiceLogging 

20from litellm._uuid import uuid 

21from litellm.constants import ( 

22 CLIENT_OUTPUT_CEILING_METADATA_KEY, 

23 CONSUMED_REQUEST_TAGS_METADATA_KEY, 

24 INTERNAL_CALL_ORIGIN_METADATA_KEY, 

25 LITELLM_PROXY_MASTER_KEY_ALIAS, 

26 OTEL_SERVICE_NAME_METADATA_KEYS, 

27 PRE_CALL_EXECUTED_GUARDRAILS_KEY, 

28 ROUTER_USAGE_COUNTED_TOKENS_METADATA_KEY, 

29 ROUTING_REQUEST_TAGS_METADATA_KEY, 

30 SESSION_DEPLOYMENT_AFFINITY_TTL_METADATA_KEY, 

31 SESSION_ID_GENERATED_METADATA_KEY, 

32 SESSION_ID_OMITTED_METADATA_KEY, 

33 X_LITELLM_DISABLE_CALLBACKS, 

34) 

35from litellm.litellm_core_utils.core_helpers import is_codex_user_agent 

36from litellm.litellm_core_utils.credential_accessor import CredentialAccessor 

37from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( 

38 TRUSTED_CALLBACK_VARS_FIELD, 

39 _request_blocked_callback_params, 

40 iter_client_callback_metadata_dicts, 

41) 

42from litellm.litellm_core_utils.internal_call_metadata import MODEL_ACCESS_GROUP_METADATA_KEY 

43from litellm.litellm_core_utils.safe_json_loads import safe_json_loads 

44from litellm.litellm_core_utils.url_utils import ( 

45 is_url_destination_allowed_by_host, 

46 provider_url_destination_candidates, 

47) 

48from litellm.proxy._types import ( 

49 AddTeamCallback, 

50 CommonProxyErrors, 

51 LitellmDataForBackendLLMCall, 

52 LiteLLMRoutes, 

53 LitellmUserRoles, 

54 ProxyErrorTypes, 

55 ProxyException, 

56 SpecialHeaders, 

57 TeamCallbackMetadata, 

58 UserAPIKeyAuth, 

59) 

60from litellm.proxy.auth.auth_utils import get_request_route 

61from litellm.proxy.auth.route_checks import RouteChecks 

62from litellm.proxy.common_utils.callback_utils import ( 

63 decrypt_callback_vars, 

64 get_metadata_variable_name_from_kwargs, 

65 strip_callback_config, 

66) 

67from litellm.proxy.common_utils.http_parsing_utils import _safe_get_request_headers 

68from litellm.proxy.spend_tracking.carried_budget_state import carried_budget_metadata 

69from litellm.types.integrations.anthropic_cache_control_hook import GATEWAY_INJECTED_CACHE_METADATA_KEY 

70 

71# Cache special headers as a frozenset for O(1) lookup performance 

72_SPECIAL_HEADERS_CACHE: Final = frozenset(str(v.value).lower() for v in SpecialHeaders) 

73 

74_REDACTED_HEADER_VALUE: Final = "***REDACTED***" 

75_CREDENTIAL_HEADER_NAMES: Final = SpecialHeaders.litellm_credential_header_names() | frozenset( 

76 {"cookie", "proxy-authorization"} 

77) 

78_TRANSPORT_ONLY_CREDENTIAL_KEYS: Final = frozenset({"provider_specific_header", "headers", "api_key"}) 

79 

80# Matches any header of the form x-<something>-session-id (case-insensitive). 

81# Excludes the two explicit litellm headers which are handled with higher priority. 

82_GENERIC_SESSION_ID_HEADER_RE: Final = re.compile(r"^x-.+-session-id$", re.IGNORECASE) 

83_EXPLICIT_SESSION_HEADERS: Final = frozenset({"x-litellm-trace-id", "x-litellm-session-id"}) 

84# Codex carries its conversation uuid in unprefixed headers, so the 

85# x-<vendor>-session-id convention above never matches it. Current builds send 

86# ``session-id``/``thread-id``; builds before the codex-api split sent 

87# ``session_id``/``conversation_id``. Ordered session before thread. 

88_CODEX_SESSION_ID_HEADERS: Final = ("session-id", "session_id", "thread-id", "conversation_id") 

89# Session-id values must be non-empty strings of alphanumerics, hyphens, or underscores 

90# (covers UUIDs and most common session-id formats). 

91_SESSION_ID_VALUE_RE: Final = re.compile(r"^[a-zA-Z0-9_\-]{8,}$") 

92 

93_SHA256_HEX_RE: Final = re.compile(r"^[0-9a-f]{64}$") 

94 

95# W3C Trace Context traceparent header: https://www.w3.org/TR/trace-context/ 

96# e.g. "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" 

97_TRACEPARENT_RE: Final = re.compile(r"^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$", re.IGNORECASE) 

98 

99 

100def _trace_id_from_traceparent(traceparent: str) -> str | None: 

101 """Extract the trace-id from a W3C Trace Context traceparent header, e.g. 

102 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" -> the 32-hex 

103 trace-id in the middle. An all-zero trace-id is invalid per spec and is 

104 rejected, matching how the OpenTelemetry SDK itself treats it.""" 

105 match: Final = _TRACEPARENT_RE.match(traceparent.strip()) 

106 if not match: 

107 return None 

108 trace_id: Final = match.group(1).lower() 

109 return trace_id if trace_id != "0" * 32 else None 

110 

111 

112def _trace_id_from_otel_span(span: "OtelSpan | None") -> str | None: 

113 if span is None: 113 ↛ 115line 113 didn't jump to line 115 because the condition on line 113 was always true

114 return None 

115 try: 

116 span_context: Final = span.get_span_context() 

117 is_valid: Final = span_context.is_valid 

118 trace_id: Final = span_context.trace_id 

119 except AttributeError: 

120 return None 

121 if not is_valid or not isinstance(trace_id, int): 

122 return None 

123 return format(trace_id, "032x") 

124 

125 

126def add_otel_trace_id_to_request( 

127 data: dict[str, object], _metadata_variable_name: str, parent_otel_span: "OtelSpan | None" 

128) -> None: 

129 if data.get("litellm_trace_id"): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true

130 return 

131 metadata: Final = data.get(_metadata_variable_name) 

132 requester_metadata: Final = data.get("metadata") 

133 if any(isinstance(m, dict) and m.get("trace_id") for m in (metadata, requester_metadata)): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true

134 return 

135 trace_id: Final = _trace_id_from_otel_span(parent_otel_span) 

136 if trace_id is None: 136 ↛ 138line 136 didn't jump to line 138 because the condition on line 136 was always true

137 return 

138 data["litellm_trace_id"] = trace_id # rebind-ok: data is an out-param 

139 if isinstance(metadata, dict): 

140 metadata["trace_id"] = trace_id 

141 

142 

143def _session_id_from_baggage(baggage: str) -> str | None: 

144 """Extract a session.id entry from a W3C Baggage header 

145 (https://www.w3.org/TR/baggage/), e.g. "session.id=abc-123,user.id=42".""" 

146 for pair in baggage.split(","): 

147 key, _, value = pair.strip().partition("=") 

148 if key.strip() == "session.id" and value.strip(): 

149 return value.strip() 

150 return None 

151 

152 

153def _stampable_key_hash(user_api_key_dict: UserAPIKeyAuth) -> str | None: 

154 """Only proxy-validated keys are stamped, proven by the unforgeable 

155 via_virtual_key marker AND a known non-secret shape: the sha256 hex digest 

156 UserAPIKeyAuth stores virtual keys in, or the master key's stable alias. 

157 Custom-auth credentials arrive raw (never forward auth material) and hashed 

158 JWTs rotate on re-issue (useless as a stable ban id), so both are skipped.""" 

159 api_key: Final = user_api_key_dict.api_key 

160 if not user_api_key_dict.via_virtual_key or api_key is None: 

161 return None 

162 if api_key == LITELLM_PROXY_MASTER_KEY_ALIAS or _SHA256_HEX_RE.fullmatch(api_key): 

163 return api_key 

164 return None 

165 

166 

167_ANTHROPIC_SESSION_ID_VALUE_RE: Final = re.compile(r"^[a-zA-Z0-9_\-]+$") 

168 

169 

170def _sanitize_for_log(value: object) -> str: 

171 """ 

172 Basic log sanitization helper to reduce log-injection risk. 

173 

174 Removes newline and carriage-return characters so user-controlled 

175 values cannot forge additional log lines when written to text logs. 

176 """ 

177 try: 

178 text = str(value) 

179 except Exception: 

180 # Fallback to repr if str() fails for any reason 

181 text = repr(value) 

182 # Strip CR/LF characters commonly used for log injection 

183 return text.replace("\r", "").replace("\n", "") 

184 

185 

186from litellm.router import Router 

187from litellm.secret_managers.main import get_secret_bool 

188from litellm.types.llms.anthropic import ANTHROPIC_API_HEADERS 

189from litellm.types.services import ServiceTypes 

190from litellm.types.utils import ( 

191 CustomPricingLiteLLMParams, 

192 LlmProviders, 

193 ProviderSpecificHeader, 

194 StandardCallbackDynamicParams, 

195 StandardLoggingUserAPIKeyMetadata, 

196 SupportedCacheControls, 

197) 

198 

199service_logger_obj: Final = ServiceLogging() # used for tracking latency on OTEL 

200# Bounded dedup for stale-alias warnings (FIFO eviction when over cap). 

201_MAX_STALE_ALIAS_WARNING_KEYS: Final = 10_000 

202_STALE_TEAM_ALIAS_WARNING_KEYS: Final[OrderedDict[str, None]] = OrderedDict() 

203# Cache the stale alias bypass flag at module load to avoid hot-path secret lookups 

204_ENABLE_TEAM_STALE_ALIAS_BYPASS: bool | None = None 

205 

206 

207if TYPE_CHECKING: 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true

208 from opentelemetry.trace import Span as OtelSpan 

209 

210 from litellm.integrations.otel.model.destination import OtelDestination 

211 from litellm.proxy.policy_engine.attachment_registry import AttachmentRegistry 

212 from litellm.proxy.proxy_server import ProxyConfig as _ProxyConfig 

213 from litellm.types.proxy.policy_engine import Policy, PolicyMatchContext 

214 

215 ProxyConfig = _ProxyConfig 

216else: 

217 ProxyConfig = Any 

218 PolicyMatchContext = Any 

219 

220 

221def parse_cache_control(cache_control): 

222 cache_dict: Final = {} 

223 directives: Final = cache_control.split(", ") 

224 

225 for directive in directives: 

226 if "=" in directive: 

227 key, value = directive.split("=") 

228 cache_dict[key] = value 

229 else: 

230 cache_dict[directive] = True 

231 

232 return cache_dict 

233 

234 

235LITELLM_METADATA_ROUTES: Final = ( 

236 "batches", 

237 "bedrock", 

238 "/v1/messages", 

239 "responses", 

240 "files", 

241) 

242 

243LITELLM_TRACE_CONTROL_METADATA_FIELDS: Final = frozenset( 

244 { 

245 "mask_input", 

246 "mask_output", 

247 "session_id", 

248 "trace_id", 

249 "trace_metadata", 

250 "trace_name", 

251 "trace_release", 

252 "trace_user_id", 

253 "trace_version", 

254 } 

255) 

256 

257_UNTRUSTED_ROOT_CONTROL_FIELDS: Final = ( 

258 "weights", 

259 "_router_weights", 

260 "proxy_server_request", 

261 "standard_logging_object", 

262 "secret_fields", 

263 "mock_response", 

264 "mock_tool_calls", 

265 "disable_global_guardrails", 

266 "disable_global_guardrail", 

267 "enable_prompt_caching", 

268 "opted_out_global_guardrails", 

269 "applied_guardrails", 

270 "applied_policies", 

271 "policy_sources", 

272 "guardrail_scan_ids", 

273 "guardrail_scan_metadata", 

274 "routing_decision", 

275 GATEWAY_INJECTED_CACHE_METADATA_KEY, 

276 "pillar_response_headers", 

277 "_guardrail_pipelines", 

278 "_pipeline_managed_guardrails", 

279 # Callback-registration fields. ``callbacks``, ``service_callback``, 

280 # and ``logger_fn`` are read by ``litellm.utils.function_setup`` and 

281 # appended to process-wide ``litellm.{input,success,failure,_async_*, 

282 # service}_callback`` lists / ``litellm.user_logger_fn`` — one request 

283 # poisons the worker for every subsequent caller. 

284 # ``litellm_disabled_callbacks`` is the inverse primitive: the 

285 # legitimate path reads it from key/team metadata, the request-body 

286 # version silently turns off admin-configured audit/observability 

287 # for the caller's request. 

288 "callbacks", 

289 "service_callback", 

290 "logger_fn", 

291 "litellm_disabled_callbacks", 

292 # Agentic-loop control fields. These bound or drive an interceptor's agentic 

293 # loop (web search, compression, code interpreter) and are server-controlled. 

294 # A client-supplied value would forge loop depth/cycle state, mark an 

295 # interception as active (triggering sandbox code execution without the 

296 # native tool ever being present), force the completed response to be 

297 # re-wrapped as a synthetic stream the caller never asked for, or raise the 

298 # loop ceiling to drive many upstream model calls and sandbox executions 

299 # from a single request. 

300 "_agentic_loop_depth", 

301 "_agentic_loop_fingerprints", 

302 "_code_interpreter_interception_active", 

303 "_code_interpreter_interception_converted_stream", 

304 "_code_interpreter_interception_sandbox_key", 

305 "_code_interpreter_interception_session_scoped", 

306 "_headroom_interception_converted_stream", 

307 "max_agentic_loops", 

308 # Recomputed below from the actual caller-controlled timeout sources (headers and 

309 # body fields); a client-forged value here would let a request either dodge cooldown 

310 # protection on a real deployment failure or force a false "not caller-controlled" 

311 # reading that lets its own bad timeout cool down deployments other tenants rely on. 

312 "client_side_timeout", 

313) 

314 

315_UNTRUSTED_METADATA_CONTROL_FIELDS: Final = ( 

316 "disable_global_guardrails", 

317 "disable_global_guardrail", 

318 "opted_out_global_guardrails", 

319 "pillar_response_headers", 

320 "_pillar_response_headers_trusted", 

321 "pillar_flagged", 

322 "pillar_scanners", 

323 "pillar_evidence", 

324 "pillar_evidence_truncated", 

325 "pillar_session_id_response", 

326 "applied_guardrails", 

327 "applied_policies", 

328 "policy_sources", 

329 "guardrail_scan_ids", 

330 "guardrail_scan_metadata", 

331 "routing_decision", 

332 GATEWAY_INJECTED_CACHE_METADATA_KEY, 

333 SESSION_DEPLOYMENT_AFFINITY_TTL_METADATA_KEY, 

334 CONSUMED_REQUEST_TAGS_METADATA_KEY, 

335 ROUTING_REQUEST_TAGS_METADATA_KEY, 

336 INTERNAL_CALL_ORIGIN_METADATA_KEY, 

337 "standard_logging_object", 

338 "proxy_server_request", 

339 "secret_fields", 

340 "_guardrail_pipelines", 

341 "_pipeline_managed_guardrails", 

342 "client_disconnected", 

343 "error_information", 

344 PRE_CALL_EXECUTED_GUARDRAILS_KEY, 

345) 

346 

347UNTRUSTED_REQUEST_HEADER_CONTROL_FIELDS: Final = frozenset( 

348 { 

349 "litellm-disable-message-redaction", 

350 } 

351) 

352_CLIENT_MOCK_CONTROL_FIELDS: Final = frozenset({"mock_response", "mock_tool_calls"}) 

353_ALLOW_CLIENT_MOCK_RESPONSE_METADATA_KEY: Final = "allow_client_mock_response" 

354_ALLOW_CLIENT_MESSAGE_REDACTION_OPT_OUT_METADATA_KEY: Final = "allow_client_message_redaction_opt_out" 

355 

356# Per-request pricing parameters mutate cost-tracking output and (via 

357# ``litellm.completion`` → ``register_model``) the process-wide 

358# ``litellm.model_cost`` map. Both effects belong to deployment configuration, 

359# not to user-supplied request bodies, so the proxy strips them before they 

360# reach the call path. Built from the Pydantic model so newly-added pricing 

361# fields are covered automatically. 

362_CLIENT_PRICING_CONTROL_FIELDS: Final = frozenset(CustomPricingLiteLLMParams.model_fields.keys()) 

363# ``model_info`` carries the same pricing fields when read by 

364# ``use_custom_pricing_for_model``; strip from metadata for the same reason. 

365# ``standard_logging_guardrail_information`` is proxy-written telemetry summed 

366# into response_cost and spend; a client seeding it forges (even negative) 

367# guardrail cost. 

368_CLIENT_PRICING_METADATA_FIELDS: Final = frozenset({"model_info", "standard_logging_guardrail_information"}) 

369# ``attempted_fallbacks`` and ``original_model_group`` are written by the router 

370# and read by spend logs as fact; a client value has no legitimate meaning and no 

371# key or team setting keeps it, so the strip is never gated. 

372_ROUTER_RESERVED_METADATA_FIELDS: Final = frozenset( 

373 { 

374 "attempted_fallbacks", 

375 "original_model_group", 

376 "request_retry_count", 

377 CLIENT_OUTPUT_CEILING_METADATA_KEY, 

378 ROUTER_USAGE_COUNTED_TOKENS_METADATA_KEY, 

379 } 

380) 

381_ALLOW_CLIENT_PRICING_OVERRIDE_METADATA_KEY: Final = "allow_client_pricing_override" 

382 

383# Request fields whose value, when URL-valued, becomes the outbound destination 

384# for a provider call. Letting a proxy caller pin the destination is an SSRF 

385# primitive (HuggingFace/Oobabooga `model`, Gemini files `file_id`); guard 

386# them centrally so SDK users keep working but proxy users default-deny. 

387_URL_DESTINATION_REQUEST_FIELDS: Final = ("model", "file_id") 

388 

389 

390def _reject_url_valued_destinations(data: dict[str, object]) -> None: 

391 """Reject URL-valued ``model``/``file_id`` unless admin-allowlisted. 

392 

393 Some providers (HuggingFace, Oobabooga, Gemini files) accept a URL in the 

394 identifier field and use it as the outbound destination. On the proxy that 

395 is an SSRF primitive — a low-privilege caller can point traffic at any 

396 host the proxy can reach, including internal services. Reject here at the 

397 proxy boundary so SDK users (who legitimately pass URL-valued identifiers) 

398 are unaffected, while admins can opt specific hosts back in via 

399 ``litellm.provider_url_destination_allowed_hosts``. 

400 """ 

401 for field in _URL_DESTINATION_REQUEST_FIELDS: 

402 value = data.get(field) 

403 if isinstance(value, str): 

404 reject_url_valued_destination(field, value) 

405 

406 

407def reject_url_valued_destination(field: str, value: str) -> None: 

408 """Reject a URL-valued destination identifier unless admin-allowlisted. 

409 

410 Operates on one field/value pair. ``_reject_url_valued_destinations`` applies 

411 it across ``_URL_DESTINATION_REQUEST_FIELDS`` for a request body. 

412 """ 

413 allowed_hosts: Final = getattr(litellm, "provider_url_destination_allowed_hosts", []) or [] 

414 for candidate in provider_url_destination_candidates(value): 

415 if not candidate.lower().startswith(("http://", "https://")): 415 ↛ 417line 415 didn't jump to line 417 because the condition on line 415 was always true

416 continue 

417 if is_url_destination_allowed_by_host(candidate, allowed_hosts): 

418 continue 

419 raise HTTPException( 

420 status_code=400, 

421 detail={ 

422 "error": "invalid_request", 

423 "param": field, 

424 "message": ( 

425 f"URL-valued '{field}' is not allowed. Configure custom " 

426 "endpoints with api_base instead, or add the destination " 

427 "host to `provider_url_destination_allowed_hosts` in " 

428 "litellm_settings." 

429 ), 

430 }, 

431 ) 

432 

433 

434_METADATA_JSON_TYPE_NAMES: Final[Mapping[type, str]] = MappingProxyType( 

435 {bool: "a boolean", int: "an integer", float: "a number", str: "a string", list: "an array"} 

436) 

437 

438 

439def _invalid_metadata_type_error(field: str, value: object) -> ProxyException: 

440 received_type: Final = _METADATA_JSON_TYPE_NAMES.get(type(value), f"a {type(value).__name__}") 

441 return ProxyException( 

442 message=f"Invalid type for '{field}': expected an object, but got {received_type} instead.", 

443 type=ProxyErrorTypes.bad_request_error, 

444 param=field, 

445 code=400, 

446 ) 

447 

448 

449def _normalized_metadata_object(field: str, value: object) -> Mapping[str, object]: 

450 """Return ``value`` as a metadata object or raise a 400 like OpenAI does. 

451 

452 A JSON string that parses to an object is accepted because multipart/form-data 

453 and ``extra_body`` callers can only send metadata as a string. The caller pops 

454 the raw value from the request body before validating so the failure-logging 

455 hooks that inspect the body afterwards don't crash on it and mask the 400 as a 500. 

456 """ 

457 if isinstance(value, dict): 457 ↛ 459line 457 didn't jump to line 459 because the condition on line 457 was always true

458 return value 

459 if isinstance(value, str) and isinstance((parsed := safe_json_loads(value)), dict): 

460 return parsed 

461 raise _invalid_metadata_type_error(field=field, value=value) 

462 

463 

464def _normalized_metadata_slot( 

465 request_data: MutableMapping[str, object], metadata_variable_name: str 

466) -> dict[str, object]: 

467 """Return the request's metadata slot as a dict, normalising it in place first. 

468 

469 Metadata can arrive as a JSON string (multipart/form-data, ``extra_body``). Parsing it here keeps 

470 existing entries alive through a merge instead of silently overwriting them with an empty dict. 

471 """ 

472 raw: Final = request_data.get(metadata_variable_name) 

473 if isinstance(raw, dict): 

474 return raw 

475 parsed: Final = safe_json_loads(raw) if isinstance(raw, str) else None 

476 normalized: Final[dict[str, object]] = parsed if isinstance(parsed, dict) else {} 

477 request_data[metadata_variable_name] = normalized 

478 return normalized 

479 

480 

481def _strip_untrusted_request_header_controls( 

482 headers: Any, 

483 *, 

484 allow_client_message_redaction_opt_out: bool = False, 

485) -> None: 

486 if not isinstance(headers, dict): 486 ↛ 487line 486 didn't jump to line 487 because the condition on line 486 was never true

487 return 

488 

489 for header_name in list(headers.keys()): 

490 if isinstance(header_name, str) and header_name.lower() in UNTRUSTED_REQUEST_HEADER_CONTROL_FIELDS: 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true

491 if allow_client_message_redaction_opt_out: 

492 continue 

493 headers.pop(header_name, None) 

494 

495 

496def _is_false_like(value: object) -> bool: 

497 if isinstance(value, bool): 

498 return value is False 

499 if isinstance(value, str): 

500 return value.strip().lower() in {"false", "0", "no", "off"} 

501 return False 

502 

503 

504def _key_or_team_metadata_flag_is_true( 

505 user_api_key_dict: UserAPIKeyAuth, 

506 metadata_key: str, 

507) -> bool: 

508 for admin_metadata in (user_api_key_dict.metadata, user_api_key_dict.team_metadata): 

509 if isinstance(admin_metadata, dict) and admin_metadata.get(metadata_key) is True: 509 ↛ 510line 509 didn't jump to line 510 because the condition on line 509 was never true

510 return True 

511 return False 

512 

513 

514def _key_or_team_allows_client_mock_response( 

515 user_api_key_dict: UserAPIKeyAuth, 

516) -> bool: 

517 return _key_or_team_metadata_flag_is_true( 

518 user_api_key_dict=user_api_key_dict, 

519 metadata_key=_ALLOW_CLIENT_MOCK_RESPONSE_METADATA_KEY, 

520 ) 

521 

522 

523def _key_or_team_allows_client_message_redaction_opt_out( 

524 user_api_key_dict: UserAPIKeyAuth, 

525) -> bool: 

526 return _key_or_team_metadata_flag_is_true( 

527 user_api_key_dict=user_api_key_dict, 

528 metadata_key=_ALLOW_CLIENT_MESSAGE_REDACTION_OPT_OUT_METADATA_KEY, 

529 ) 

530 

531 

532def _key_or_team_allows_client_pricing_override( 

533 user_api_key_dict: UserAPIKeyAuth, 

534) -> bool: 

535 return _key_or_team_metadata_flag_is_true( 

536 user_api_key_dict=user_api_key_dict, 

537 metadata_key=_ALLOW_CLIENT_PRICING_OVERRIDE_METADATA_KEY, 

538 ) 

539 

540 

541def _strip_client_message_redaction_opt_out(data: dict[str, object]) -> None: 

542 stripped: Final[list[str]] = [] 

543 if "turn_off_message_logging" in data and _is_false_like(data["turn_off_message_logging"]): 

544 stripped.append("turn_off_message_logging") 

545 data.pop("turn_off_message_logging", None) 

546 for slot_label, metadata in iter_client_callback_metadata_dicts(data): 

547 if "turn_off_message_logging" in metadata and _is_false_like(metadata["turn_off_message_logging"]): 

548 stripped.append(f"{slot_label}.turn_off_message_logging") 

549 metadata.pop("turn_off_message_logging", None) 

550 if stripped: 

551 verbose_proxy_logger.debug( 

552 "Stripped client-supplied message-redaction opt-out fields from request body: %s. " 

553 "Set `allow_client_message_redaction_opt_out: true` on the key or team metadata " 

554 "to keep these values.", 

555 ", ".join(stripped), 

556 ) 

557 

558 

559def _strip_client_callback_credentials( 

560 data: dict[str, Any], # mutable-ok: strips in place on the request body the pre-call pipeline threads through 

561) -> None: 

562 """Drop callback credentials and destinations supplied by the caller. 

563 

564 ``_request_blocked_callback_params`` (Datadog + GCS credentials, sites and agent 

565 hosts) are already ignored when building ``standard_callback_dynamic_params``. 

566 Strip them from the body and every client metadata slot as well, so a caller 

567 cannot pair its own ``dd_site``/``dd_agent_host`` with the team's admin-configured 

568 ``dd_api_key`` and have the resulting logs shipped to a host it controls. 

569 

570 ``TRUSTED_CALLBACK_VARS_FIELD`` is proxy-owned; it is cleared here and repopulated 

571 from team/key callback settings in ``add_litellm_data_to_request``. 

572 """ 

573 containers: Final = (("body", data), *iter_client_callback_metadata_dicts(data)) 

574 stripped: Final = tuple( 

575 f"{label}.{field}" 

576 for label, container in containers 

577 for field in _request_blocked_callback_params 

578 if field in container 

579 ) 

580 for _, container in containers: 

581 for field in _request_blocked_callback_params: 

582 container.pop(field, None) 

583 data.pop(TRUSTED_CALLBACK_VARS_FIELD, None) 

584 if stripped: 584 ↛ 585line 584 didn't jump to line 585 because the condition on line 584 was never true

585 verbose_proxy_logger.debug( 

586 "Stripped client-supplied callback credentials from request: %s. " 

587 "Configure these on the team or key callback settings instead.", 

588 ", ".join(sorted(stripped)), 

589 ) 

590 

591 

592def _strip_client_pricing_overrides(data: dict[str, object]) -> None: 

593 """Drop pricing overrides from the request body and any metadata variant. 

594 

595 Skipped only when the calling key/team carries 

596 ``allow_client_pricing_override: True`` in its metadata. Emits a 

597 ``debug``-level log line naming the dropped fields so operators can 

598 trace why a client-supplied pricing override stopped being applied 

599 (otherwise the strip is invisible from the caller's perspective). 

600 """ 

601 stripped: Final[list[str]] = [] 

602 for field in _CLIENT_PRICING_CONTROL_FIELDS: 

603 if field in data: 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true

604 stripped.append(field) 

605 data.pop(field, None) 

606 for metadata_key in ("metadata", "litellm_metadata"): 

607 metadata = data.get(metadata_key) 

608 if not isinstance(metadata, dict): 

609 continue 

610 for field in _CLIENT_PRICING_METADATA_FIELDS: 

611 if field in metadata: 611 ↛ 612line 611 didn't jump to line 612 because the condition on line 611 was never true

612 stripped.append(f"{metadata_key}.{field}") 

613 metadata.pop(field, None) 

614 if stripped: 614 ↛ 615line 614 didn't jump to line 615 because the condition on line 614 was never true

615 verbose_proxy_logger.debug( 

616 "Stripped client-supplied pricing fields from request body: %s. " 

617 "Set `allow_client_pricing_override: true` on the key or team " 

618 "metadata to keep these values.", 

619 ", ".join(stripped), 

620 ) 

621 

622 

623def _strip_router_reserved_metadata( 

624 data: dict[str, Any], # mutable-ok: strips in place on the request body the pre-call pipeline threads through 

625) -> None: 

626 """Drop the router-owned fallback stamps from any client-supplied metadata bucket.""" 

627 for metadata_key in ("metadata", "litellm_metadata"): 

628 if not isinstance(metadata := data.get(metadata_key), dict): 

629 continue 

630 for field in _ROUTER_RESERVED_METADATA_FIELDS & metadata.keys(): 630 ↛ 631line 630 didn't jump to line 631 because the loop on line 630 never started

631 metadata.pop(field) 

632 verbose_proxy_logger.debug( 

633 "Stripped router-reserved metadata field from request body: %s.%s", metadata_key, field 

634 ) 

635 

636 

637def _get_metadata_variable_name(request: Request) -> str: 

638 """ 

639 Helper to return what the "metadata" field should be called in the request data 

640 

641 For all /thread or /assistant endpoints we need to call this "litellm_metadata" 

642 

643 For ALL other endpoints we call this "metadata" 

644 """ 

645 # Inline imports — auth_utils/route_checks participate in a proxy import cycle. 

646 from litellm.proxy.auth.auth_utils import get_request_route # noqa: PLC0415 

647 

648 path: Final = get_request_route(request) 

649 if "thread" in path or "assistant" in path: 

650 return "litellm_metadata" 

651 

652 if any(route in path for route in LITELLM_METADATA_ROUTES): 

653 return "litellm_metadata" 

654 

655 return "metadata" 

656 

657 

658def _promoted_trace_control_fields( 

659 requester_metadata: Mapping[str, object], 

660 litellm_metadata: Mapping[str, object], 

661) -> tuple[tuple[str, object], ...]: 

662 """Return the caller's trace-control fields that ``litellm_metadata`` does not already set.""" 

663 return tuple( 

664 (key, value) 

665 for key, value in requester_metadata.items() 

666 if key in LITELLM_TRACE_CONTROL_METADATA_FIELDS and key not in litellm_metadata 

667 ) 

668 

669 

670def _extract_generic_session_id_from_headers( 

671 normalized: dict[str, str], 

672) -> str | None: 

673 """ 

674 Scan a normalised (lower-cased keys) header dict for any header that looks 

675 like ``x-<vendor>-session-id`` and whose value is a plausible session/trace 

676 identifier (alphanumeric + hyphens/underscores, at least 8 chars). 

677 

678 The two explicit LiteLLM headers (``x-litellm-trace-id`` / 

679 ``x-litellm-session-id``) are excluded here because they are handled with 

680 higher priority by the caller. 

681 

682 Example: ``x-claude-code-session-id: e96634a3-fa28-4083-b354-55542e2dca01`` 

683 """ 

684 for key, value in normalized.items(): 

685 if ( 685 ↛ 691line 685 didn't jump to line 691 because the condition on line 685 was never true

686 key not in _EXPLICIT_SESSION_HEADERS 

687 and _GENERIC_SESSION_ID_HEADER_RE.match(key) 

688 and isinstance(value, str) 

689 and _SESSION_ID_VALUE_RE.match(value) 

690 ): 

691 return value 

692 return None 

693 

694 

695def _extract_codex_session_id_from_headers( 

696 normalized: Mapping[str, str], 

697) -> str | None: 

698 """ 

699 Read Codex's conversation uuid off one of ``_CODEX_SESSION_ID_HEADERS``. 

700 

701 Codex sends no request metadata the Anthropic path could parse and no 

702 ``x-``-prefixed session header, so without this every turn of a Codex session 

703 falls through to a freshly generated per-call trace id and lands as its own 

704 row in the logs instead of grouping. 

705 

706 Unprefixed names like ``session-id`` are generic enough that another client 

707 could send one meaning something unrelated, and colliding values across 

708 callers would merge their traces, so this only applies to callers that 

709 identify as Codex. 

710 """ 

711 user_agent: Final = normalized.get("user-agent") 

712 if not isinstance(user_agent, str) or not is_codex_user_agent(user_agent): 712 ↛ 714line 712 didn't jump to line 714 because the condition on line 712 was always true

713 return None 

714 return next( 

715 ( 

716 value 

717 for value in (normalized.get(header) for header in _CODEX_SESSION_ID_HEADERS) 

718 if isinstance(value, str) and _SESSION_ID_VALUE_RE.match(value) 

719 ), 

720 None, 

721 ) 

722 

723 

724def _extract_bare_session_id_from_headers( 

725 normalized: Mapping[str, str], 

726) -> str | None: 

727 """ 

728 Read a vendor-less ``x-session-id`` header (opencode sends ``X-Session-Id`` 

729 alongside ``x-session-affinity`` on every turn of a session). Checked after 

730 the ``x-<vendor>-session-id`` scan so a more specific header such as 

731 opencode's ``x-parent-session-id`` on subagent calls keeps winning. 

732 """ 

733 value: Final = normalized.get("x-session-id") 

734 if isinstance(value, str) and _SESSION_ID_VALUE_RE.match(value): 734 ↛ 735line 734 didn't jump to line 735 because the condition on line 734 was never true

735 return value 

736 return None 

737 

738 

739def get_chain_id_from_headers(headers: dict[str, str] | None) -> str | None: 

740 """ 

741 Extract chain id for call chaining from request headers. 

742 

743 Priority order: 

744 1. ``x-litellm-trace-id`` (explicit, highest priority) 

745 2. ``x-litellm-session-id`` (explicit) 

746 3. Any ``x-<vendor>-session-id`` header whose value looks like a session id 

747 (alphanumeric / UUID, at least 8 chars). E.g. ``x-claude-code-session-id``. 

748 4. Codex's unprefixed ``session-id`` / ``thread-id``, for Codex callers only. 

749 5. A vendor-less ``x-session-id`` header (e.g. opencode), same value rules. 

750 

751 Header keys are matched case-insensitively so this works with raw header 

752 dicts from any transport. 

753 

754 Used by MCP (and other paths that have raw_headers but no Request) to set 

755 litellm_trace_id/litellm_session_id for spend logs and logging consistency. 

756 """ 

757 if not headers: 757 ↛ 758line 757 didn't jump to line 758 because the condition on line 757 was never true

758 return None 

759 normalized: Final = {k.lower(): v for k, v in headers.items() if isinstance(k, str)} 

760 return ( 

761 normalized.get("x-litellm-trace-id") 

762 or normalized.get("x-litellm-session-id") 

763 or _extract_generic_session_id_from_headers(normalized) 

764 or _extract_codex_session_id_from_headers(normalized) 

765 or _extract_bare_session_id_from_headers(normalized) 

766 ) 

767 

768 

769def _get_anthropic_session_id_from_metadata(metadata: object) -> str | None: 

770 if not isinstance(metadata, dict): 

771 return None 

772 

773 user_id: Final = metadata.get("user_id") 

774 if isinstance(user_id, dict): 774 ↛ 775line 774 didn't jump to line 775 because the condition on line 774 was never true

775 session_id = user_id.get("session_id") 

776 if isinstance(session_id, str) and _ANTHROPIC_SESSION_ID_VALUE_RE.fullmatch(session_id): 

777 return session_id 

778 return None 

779 if not isinstance(user_id, str): 779 ↛ 782line 779 didn't jump to line 782 because the condition on line 779 was always true

780 return None 

781 

782 session_marker: Final = "_session_" 

783 session_marker_index: Final = user_id.rfind(session_marker) 

784 if session_marker_index == -1: 

785 return None 

786 

787 session_id = user_id[session_marker_index + len(session_marker) :] 

788 if not session_id or not _ANTHROPIC_SESSION_ID_VALUE_RE.fullmatch(session_id): 

789 return None 

790 return session_id 

791 

792 

793def _is_llm_inference_route(request: Request) -> bool: 

794 route: Final = get_request_route(request) 

795 return RouteChecks.is_llm_api_route(route=route) and not RouteChecks.check_route_access( 

796 route=route, allowed_routes=LiteLLMRoutes.mcp_routes.value 

797 ) 

798 

799 

800def apply_missing_session_id_policy( 

801 data: dict[str, object], # mutable-ok: stamps session ids in place on the request body the pipeline threads through 

802 _metadata_variable_name: str, 

803 general_settings: Mapping[str, object] | None, 

804 request: Request, 

805) -> None: 

806 for metadata_key in ("metadata", "litellm_metadata"): 

807 if isinstance(client_metadata := data.get(metadata_key), dict): 

808 client_metadata.pop(SESSION_ID_OMITTED_METADATA_KEY, None) 

809 metadata: Final = data.get(_metadata_variable_name) 

810 policy: Final = general_settings.get("missing_session_id") if general_settings else None 

811 if policy is None or not _is_llm_inference_route(request): 811 ↛ 813line 811 didn't jump to line 813 because the condition on line 811 was always true

812 return 

813 if not isinstance(metadata, dict): 

814 return 

815 if policy == "omit": 

816 metadata[SESSION_ID_OMITTED_METADATA_KEY] = True 

817 requester_metadata: Final = data.get("metadata") 

818 requester_session_id: Final = ( 

819 requester_metadata.get("session_id") if isinstance(requester_metadata, dict) else None 

820 ) 

821 if ( 

822 (body_session_id := data.get("litellm_session_id")) 

823 and not metadata.get("session_id") 

824 and not requester_session_id 

825 ): 

826 metadata["session_id"] = body_session_id 

827 return 

828 if data.get("litellm_session_id") or metadata.get("session_id"): 

829 return 

830 match policy: 

831 case "generate": 

832 session_id: Final = str(data.get("litellm_trace_id") or metadata.get("trace_id") or uuid.uuid4()) 

833 data["litellm_session_id"] = session_id # rebind-ok: data is an out-param 

834 data.setdefault("litellm_trace_id", session_id) 

835 metadata["session_id"] = session_id 

836 metadata[SESSION_ID_GENERATED_METADATA_KEY] = True 

837 case "reject": 

838 raise ProxyException( 

839 message=( 

840 "Request has no session id. Send an `x-litellm-session-id` header or `metadata.session_id`. " 

841 "Required by `general_settings.missing_session_id: reject`." 

842 ), 

843 type=ProxyErrorTypes.bad_request_error, 

844 param="session_id", 

845 code=400, 

846 ) 

847 case _: 

848 verbose_proxy_logger.warning( 

849 "Ignoring unknown general_settings.missing_session_id=%r; expected 'generate', 'reject' or 'omit'", 

850 policy, 

851 ) 

852 

853 

854def should_auto_drop_params_for_agentic_cli(user_agent: str, data: dict, proxy_config: ProxyConfig) -> bool: 

855 """drop_params defaults to on for agentic CLIs so their client-specific 

856 params (e.g. Claude Code's thinking, Codex's service_tier) don't fail 

857 requests routed to providers that reject them. An explicit drop_params 

858 from the caller or in the operator's ``litellm_settings`` always wins 

859 over this default.""" 

860 from litellm.llms.anthropic.common_utils import is_claude_code_user_agent 

861 

862 if not (is_claude_code_user_agent(user_agent) or is_codex_user_agent(user_agent)): 862 ↛ 864line 862 didn't jump to line 864 because the condition on line 862 was always true

863 return False 

864 if "drop_params" in data: 

865 return False 

866 config: Final = getattr(proxy_config, "config", None) 

867 litellm_settings: Final = config.get("litellm_settings") if isinstance(config, dict) else None 

868 return not (isinstance(litellm_settings, dict) and "drop_params" in litellm_settings) 

869 

870 

871def safe_add_api_version_from_query_params(data: dict, request: Request): 

872 try: 

873 if hasattr(request, "query_params"): 873 ↛ exitline 873 didn't return from function 'safe_add_api_version_from_query_params' because the condition on line 873 was always true

874 query_params: Final = dict(request.query_params) 

875 if "api-version" in query_params: 875 ↛ 876line 875 didn't jump to line 876 because the condition on line 875 was never true

876 data["api_version"] = query_params["api-version"] 

877 except KeyError: 

878 pass 

879 except Exception as e: 

880 verbose_logger.exception("error checking api version in query params: %s", str(e)) 

881 

882 

883def convert_key_logging_metadata_to_callback( 

884 data: AddTeamCallback, 

885 team_callback_settings_obj: TeamCallbackMetadata | None, 

886) -> TeamCallbackMetadata: 

887 if team_callback_settings_obj is None: 887 ↛ 888line 887 didn't jump to line 888 because the condition on line 887 was never true

888 team_callback_settings_obj = TeamCallbackMetadata() 

889 if data.callback_type == "success": 889 ↛ 890line 889 didn't jump to line 890 because the condition on line 889 was never true

890 if team_callback_settings_obj.success_callback is None: 

891 team_callback_settings_obj.success_callback = [] 

892 

893 if data.callback_name not in team_callback_settings_obj.success_callback: 

894 team_callback_settings_obj.success_callback.append(data.callback_name) 

895 elif data.callback_type == "failure": 895 ↛ 896line 895 didn't jump to line 896 because the condition on line 895 was never true

896 if team_callback_settings_obj.failure_callback is None: 

897 team_callback_settings_obj.failure_callback = [] 

898 

899 if data.callback_name not in team_callback_settings_obj.failure_callback: 

900 team_callback_settings_obj.failure_callback.append(data.callback_name) 

901 elif ( 901 ↛ 920line 901 didn't jump to line 920 because the condition on line 901 was always true

902 not data.callback_type or data.callback_type == "success_and_failure" 

903 ): # assume 'success_and_failure' = litellm.callbacks 

904 if team_callback_settings_obj.success_callback is None: 904 ↛ 905line 904 didn't jump to line 905 because the condition on line 904 was never true

905 team_callback_settings_obj.success_callback = [] 

906 if team_callback_settings_obj.failure_callback is None: 906 ↛ 907line 906 didn't jump to line 907 because the condition on line 906 was never true

907 team_callback_settings_obj.failure_callback = [] 

908 if team_callback_settings_obj.callbacks is None: 908 ↛ 909line 908 didn't jump to line 909 because the condition on line 908 was never true

909 team_callback_settings_obj.callbacks = [] 

910 

911 if data.callback_name not in team_callback_settings_obj.success_callback: 

912 team_callback_settings_obj.success_callback.append(data.callback_name) 

913 

914 if data.callback_name not in team_callback_settings_obj.failure_callback: 

915 team_callback_settings_obj.failure_callback.append(data.callback_name) 

916 

917 if data.callback_name not in team_callback_settings_obj.callbacks: 

918 team_callback_settings_obj.callbacks.append(data.callback_name) 

919 

920 for var, value in data.callback_vars.items(): 920 ↛ 925line 920 didn't jump to line 925 because the loop on line 920 never started

921 # New Relic routing reads these from the trusted-vars overlay with no 

922 # callback-name check, so scope them to the newrelic entry: a team that 

923 # put newrelic_* under a different callback never asked for New Relic and 

924 # must not export to it. 

925 if var.startswith("newrelic_") and data.callback_name != "newrelic": 

926 continue 

927 if team_callback_settings_obj.callback_vars is None: 

928 team_callback_settings_obj.callback_vars = {} 

929 team_callback_settings_obj.callback_vars[var] = str(value) 

930 

931 return team_callback_settings_obj 

932 

933 

934def _get_validated_callback_metadata(item: dict, *, source: str) -> AddTeamCallback | None: 

935 try: 

936 return AddTeamCallback(**item) 

937 except (PydanticValidationError, ValueError) as e: 

938 verbose_proxy_logger.warning( 

939 "Ignoring invalid %s callback metadata: %s", 

940 source, 

941 _sanitize_for_log(str(e)), 

942 ) 

943 return None 

944 

945 

946class KeyAndTeamLoggingSettings: 

947 """ 

948 Helper class to get the dynamic logging settings for the key and team 

949 """ 

950 

951 @staticmethod 

952 def get_key_dynamic_logging_settings(user_api_key_dict: UserAPIKeyAuth): 

953 if user_api_key_dict.metadata is not None and "logging" in user_api_key_dict.metadata: 953 ↛ 954line 953 didn't jump to line 954 because the condition on line 953 was never true

954 return decrypt_callback_vars(user_api_key_dict.metadata).get("logging") 

955 return None 

956 

957 @staticmethod 

958 def get_team_dynamic_logging_settings(user_api_key_dict: UserAPIKeyAuth): 

959 if user_api_key_dict.team_metadata is not None and "logging" in user_api_key_dict.team_metadata: 959 ↛ 960line 959 didn't jump to line 960 because the condition on line 959 was never true

960 return decrypt_callback_vars(user_api_key_dict.team_metadata).get("logging") 

961 return None 

962 

963 

964def _get_dynamic_logging_metadata( 

965 user_api_key_dict: UserAPIKeyAuth, proxy_config: ProxyConfig 

966) -> TeamCallbackMetadata | None: 

967 callback_settings_obj: TeamCallbackMetadata | None = None 

968 key_dynamic_logging_settings: Final[dict | None] = KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings( 

969 user_api_key_dict 

970 ) 

971 team_dynamic_logging_settings: Final[dict | None] = KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings( 

972 user_api_key_dict 

973 ) 

974 ######################################################################################### 

975 # Key-based callbacks 

976 ######################################################################################### 

977 if key_dynamic_logging_settings is not None: 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true

978 for item in key_dynamic_logging_settings: 

979 callback = _get_validated_callback_metadata(item=item, source="key-level") 

980 if callback is None: 

981 continue 

982 callback_settings_obj = convert_key_logging_metadata_to_callback( 

983 data=callback, 

984 team_callback_settings_obj=callback_settings_obj, 

985 ) 

986 ######################################################################################### 

987 # Team-based callbacks 

988 ######################################################################################### 

989 elif team_dynamic_logging_settings is not None: 989 ↛ 990line 989 didn't jump to line 990 because the condition on line 989 was never true

990 for item in team_dynamic_logging_settings: 

991 callback = _get_validated_callback_metadata(item=item, source="team-level") 

992 if callback is None: 

993 continue 

994 callback_settings_obj = convert_key_logging_metadata_to_callback( 

995 data=callback, 

996 team_callback_settings_obj=callback_settings_obj, 

997 ) 

998 ######################################################################################### 

999 # Deprecated format - maintained for backwards compatibility 

1000 ######################################################################################### 

1001 elif user_api_key_dict.team_metadata is not None and "callback_settings" in user_api_key_dict.team_metadata: 1001 ↛ 1002line 1001 didn't jump to line 1002 because the condition on line 1001 was never true

1002 """ 

1003 callback_settings = { 

1004 { 

1005 'callback_vars': {'langfuse_public_key': 'pk', 'langfuse_secret_key': 'sk_'}, 

1006 'failure_callback': [], 

1007 'success_callback': ['langfuse', 'langfuse'] 

1008 } 

1009 } 

1010 """ 

1011 team_metadata: Final = decrypt_callback_vars(user_api_key_dict.team_metadata) 

1012 callback_settings: Final = team_metadata.get("callback_settings", None) or {} 

1013 callback_settings_obj = TeamCallbackMetadata(**callback_settings) 

1014 verbose_proxy_logger.debug("Team callback settings activated: %s", callback_settings_obj) 

1015 ######################################################################################### 

1016 # Enter here when configured on the config.yaml file. 

1017 ######################################################################################### 

1018 elif user_api_key_dict.team_id is not None: 1018 ↛ 1019line 1018 didn't jump to line 1019 because the condition on line 1018 was never true

1019 callback_settings_obj = LiteLLMProxyRequestSetup.add_team_based_callbacks_from_config( 

1020 team_id=user_api_key_dict.team_id, proxy_config=proxy_config 

1021 ) 

1022 return callback_settings_obj 

1023 

1024 

1025_TENANT_OTEL_PARAMS: Final = TypeAdapter(StandardCallbackDynamicParams) 

1026 

1027 

1028def _tenant_otel_params(callback_vars: Mapping[str, str]) -> StandardCallbackDynamicParams: 

1029 try: 

1030 return _TENANT_OTEL_PARAMS.validate_python(callback_vars) 

1031 except PydanticValidationError: 

1032 return StandardCallbackDynamicParams() 

1033 

1034 

1035_NO_REQUEST_HEADERS: Final[Mapping[str, str]] = MappingProxyType({}) 

1036 

1037 

1038def _dynamically_disabled_backends( 

1039 user_api_key_dict: UserAPIKeyAuth, 

1040 request_headers: Mapping[str, str] | None, 

1041) -> frozenset[str]: 

1042 """The callbacks this request turned off, read the way dispatch reads them. 

1043 

1044 Same sources, precedence, and premium gate ``EnterpriseCallbackControls`` applies 

1045 before it skips a callback: the ``x-litellm-disable-callbacks`` header wins over the 

1046 key's stored list, team settings are not a source, and a non-premium proxy honours 

1047 neither. A destination has to agree with that decision, or a backend the key turned 

1048 off would still be exported to, now through the fan-out instead of the callback. 

1049 """ 

1050 from litellm.proxy.proxy_server import premium_user 

1051 

1052 if litellm.allow_dynamic_callback_disabling is not True or not premium_user: 

1053 return frozenset() 

1054 header: Final = (request_headers if request_headers is not None else _NO_REQUEST_HEADERS).get( 

1055 X_LITELLM_DISABLE_CALLBACKS 

1056 ) 

1057 if header is not None: 

1058 return frozenset(name.strip().lower() for name in header.split(",")) 

1059 metadata: Final = user_api_key_dict.metadata 

1060 disabled: Final = metadata.get("litellm_disabled_callbacks") if metadata else None 

1061 if not isinstance(disabled, list): 

1062 return frozenset() 

1063 return frozenset(name.lower() for name in disabled if isinstance(name, str)) 

1064 

1065 

1066def resolve_tenant_otel_destinations( 

1067 user_api_key_dict: UserAPIKeyAuth, 

1068 request_headers: Mapping[str, str] | None = None, 

1069) -> "tuple[OtelDestination, ...]": 

1070 """The OTLP destinations this request's key or team config overrides its traces to. 

1071 

1072 Key settings win over team settings outright, the same precedence 

1073 ``_get_dynamic_logging_metadata`` applies, so one caller never exports the same 

1074 backend to two accounts. An empty key-level list counts as configured, since that 

1075 is what disabling a key's callbacks writes. Returns empty when OTEL V2 is off, when 

1076 neither level named a destination-capable backend, or when the config is 

1077 incomplete, and the request then keeps the operator's own exporters. 

1078 

1079 Two entries naming the same backend merge their ``callback_vars`` last-wins, the 

1080 way ``convert_key_logging_metadata_to_callback`` merges them, so the destination 

1081 and the per-request tracer routing cannot read one config two ways. 

1082 

1083 A ``failure``-only entry is skipped: a destination is resolved during auth, before 

1084 the request has an outcome, so honouring the filter would mean holding every span 

1085 back until the call finishes. Those entries keep today's behaviour instead, where 

1086 the tenant's credentials reach the backend through per-request tracer routing and 

1087 the operator's exporter is left alone. Its ``callback_vars`` still take part in the 

1088 merge for a backend another entry made eligible, so the destination carries the 

1089 same credentials the runtime parser resolves for that request. 

1090 

1091 A backend the request disabled dynamically, through the key's 

1092 ``litellm_disabled_callbacks`` or the ``x-litellm-disable-callbacks`` header in 

1093 ``request_headers``, resolves to no destination, so the fan-out never carries the 

1094 request tree to that account and the operator's exporter is never suppressed for 

1095 it. That leaves the request exactly where it stood before destinations existed: 

1096 the OTel V2 logger itself is not on the disable list's class registry, so its own 

1097 span still routes to the tenant's credentials the way it did then. 

1098 """ 

1099 from litellm.integrations.otel.model.config import is_otel_v2_enabled 

1100 from litellm.integrations.otel.presets.destinations import destination_for 

1101 

1102 if not is_otel_v2_enabled(): 1102 ↛ 1104line 1102 didn't jump to line 1104 because the condition on line 1102 was always true

1103 return () 

1104 key_entries: Final = KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict) 

1105 entries: Final = ( 

1106 key_entries 

1107 if key_entries is not None 

1108 else KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict) 

1109 ) 

1110 if not entries: 

1111 return () 

1112 disabled: Final = _dynamically_disabled_backends(user_api_key_dict, request_headers) 

1113 callbacks: Final = tuple( 

1114 callback 

1115 for item in entries 

1116 if (callback := _get_validated_callback_metadata(item=item, source="otel-destination")) is not None 

1117 if callback.callback_name.lower() not in disabled 

1118 ) 

1119 return tuple( 

1120 destination 

1121 for name in dict.fromkeys( 

1122 callback.callback_name for callback in callbacks if callback.callback_type != "failure" 

1123 ) 

1124 if ( 

1125 destination := destination_for( 

1126 name, 

1127 _tenant_otel_params( 

1128 MappingProxyType( 

1129 { 

1130 var: value 

1131 for callback in callbacks 

1132 if callback.callback_name == name 

1133 for var, value in callback.callback_vars.items() 

1134 } 

1135 ) 

1136 ), 

1137 _tenant_service_name(user_api_key_dict), 

1138 ) 

1139 ) 

1140 is not None 

1141 ) 

1142 

1143 

1144def _tenant_service_name(user_api_key_dict: UserAPIKeyAuth) -> str | None: 

1145 """The ``service.name`` this key or team configured, the key winning over its team. 

1146 

1147 Same fields and same precedence the request-metadata build applies, read straight 

1148 off the auth object because destinations resolve during auth, before that metadata 

1149 is assembled. 

1150 """ 

1151 sources: Final = (user_api_key_dict.metadata, user_api_key_dict.team_metadata) 

1152 return next( 

1153 ( 

1154 stripped 

1155 for source in sources 

1156 if source 

1157 for field in OTEL_SERVICE_NAME_METADATA_KEYS 

1158 if isinstance(value := source.get(field), str) and (stripped := value.strip()) 

1159 ), 

1160 None, 

1161 ) 

1162 

1163 

1164def clean_headers( 

1165 headers: Headers, 

1166 litellm_key_header_name: str | None = None, 

1167 forward_llm_provider_auth_headers: bool = False, 

1168 authenticated_with_header: str | None = None, 

1169) -> dict: 

1170 """ 

1171 Removes litellm api key from headers 

1172 

1173 Args: 

1174 headers: Request headers 

1175 litellm_key_header_name: Custom header name for LiteLLM API key 

1176 forward_llm_provider_auth_headers: Whether to forward provider auth headers 

1177 authenticated_with_header: Which header was used for LiteLLM authentication 

1178 (e.g., "x-litellm-api-key", "authorization", "x-api-key") 

1179 

1180 Returns: 

1181 Cleaned headers dict 

1182 """ 

1183 from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key 

1184 

1185 clean_headers: Final = {} 

1186 litellm_key_lower: Final = litellm_key_header_name.lower() if litellm_key_header_name is not None else None 

1187 for header, value in headers.items(): 

1188 header_lower = header.lower() 

1189 

1190 if header_lower == "authorization" and is_anthropic_oauth_key(value): 1190 ↛ 1191line 1190 didn't jump to line 1191 because the condition on line 1190 was never true

1191 if authenticated_with_header is None or authenticated_with_header.lower() != "authorization": 

1192 clean_headers[header] = value 

1193 continue 

1194 # Special handling for x-api-key: forward it based on authenticated_with_header 

1195 elif header_lower == "x-api-key": 1195 ↛ 1196line 1195 didn't jump to line 1196 because the condition on line 1195 was never true

1196 if forward_llm_provider_auth_headers and ( 

1197 authenticated_with_header is None or authenticated_with_header.lower() != "x-api-key" 

1198 ): 

1199 clean_headers[header] = value 

1200 elif forward_llm_provider_auth_headers and header_lower in _SPECIAL_HEADERS_CACHE: 1200 ↛ 1201line 1200 didn't jump to line 1201 because the condition on line 1200 was never true

1201 if litellm_key_lower and header_lower == litellm_key_lower: 

1202 continue 

1203 if header_lower == "authorization": 

1204 continue 

1205 # Never forward x-litellm-api-key (it's for proxy auth only) 

1206 if header_lower == "x-litellm-api-key": 

1207 continue 

1208 clean_headers[header] = value 

1209 # Check if header should be excluded: either in special headers cache or matches custom litellm key 

1210 elif header_lower not in _SPECIAL_HEADERS_CACHE and ( 

1211 litellm_key_lower is None or header_lower != litellm_key_lower 

1212 ): 

1213 clean_headers[header] = value 

1214 return clean_headers 

1215 

1216 

1217def _is_credential_header(header: str) -> bool: 

1218 """Whether `header` carries a caller credential rather than request context.""" 

1219 return header.lower() in _CREDENTIAL_HEADER_NAMES 

1220 

1221 

1222def redact_credential_headers(headers: Mapping[str, str]) -> Mapping[str, str]: 

1223 """Return a copy of `headers` with credential-bearing values masked. 

1224 

1225 `clean_headers` deliberately preserves some credential headers so they can be 

1226 forwarded to the upstream provider; an Anthropic subscription OAuth token in 

1227 `Authorization`, or a client-supplied provider key in `x-api-key`. Those values 

1228 must never reach a logging callback or a spend log, so every observability-facing 

1229 copy of the header dict is built through this helper while the copy that is 

1230 forwarded upstream keeps the real values. 

1231 

1232 The returned object is a plain dict; guardrail hooks stamp their own headers onto 

1233 the stored copy and the logging callbacks JSON-serialize it. 

1234 """ 

1235 return { 

1236 header: (_REDACTED_HEADER_VALUE if _is_credential_header(header) else value) 

1237 for header, value in headers.items() 

1238 } 

1239 

1240 

1241class LiteLLMProxyRequestSetup: 

1242 @staticmethod 

1243 def _get_timeout_from_request(headers: dict) -> float | None: 

1244 """ 

1245 Workaround for client request from Vercel's AI SDK. 

1246 

1247 Allow's user to set a timeout in the request headers. 

1248 

1249 Example: 

1250 

1251 ```js 

1252 const openaiProvider = createOpenAI({ 

1253 baseURL: liteLLM.baseURL, 

1254 apiKey: liteLLM.apiKey, 

1255 compatibility: "compatible", 

1256 headers: { 

1257 "x-litellm-timeout": "90" 

1258 }, 

1259 }); 

1260 ``` 

1261 """ 

1262 timeout_header: Final = headers.get("x-litellm-timeout", None) 

1263 if timeout_header is not None: 1263 ↛ 1264line 1263 didn't jump to line 1264 because the condition on line 1263 was never true

1264 return float(timeout_header) 

1265 return None 

1266 

1267 @staticmethod 

1268 def _get_stream_timeout_from_request(headers: dict) -> float | None: 

1269 """ 

1270 Get the `stream_timeout` from the request headers. 

1271 """ 

1272 stream_timeout_header: Final = headers.get("x-litellm-stream-timeout", None) 

1273 if stream_timeout_header is not None: 1273 ↛ 1274line 1273 didn't jump to line 1274 because the condition on line 1273 was never true

1274 return float(stream_timeout_header) 

1275 return None 

1276 

1277 @staticmethod 

1278 def _get_keepalive_seconds_from_request(headers: Mapping[str, str]) -> float | None: 

1279 """ 

1280 Get `keepalive_seconds` from the request headers, for clients (e.g. the 

1281 Vercel AI SDK) that can set custom headers more easily than extra body 

1282 fields. Subject to the same deployment-level allow_client_keepalive_override 

1283 gate as the request body field: see _resolve_keepalive_seconds. 

1284 """ 

1285 keepalive_seconds_header: Final = headers.get("x-litellm-keepalive-seconds", None) 

1286 if keepalive_seconds_header is not None: 1286 ↛ 1287line 1286 didn't jump to line 1287 because the condition on line 1286 was never true

1287 return float(keepalive_seconds_header) 

1288 return None 

1289 

1290 @staticmethod 

1291 def _get_num_retries_from_request(headers: dict) -> int | None: 

1292 """ 

1293 Workaround for client request from Vercel's AI SDK. 

1294 """ 

1295 num_retries_header: Final = headers.get("x-litellm-num-retries", None) 

1296 if num_retries_header is not None: 1296 ↛ 1297line 1296 didn't jump to line 1297 because the condition on line 1296 was never true

1297 return int(num_retries_header) 

1298 return None 

1299 

1300 @staticmethod 

1301 def _get_spend_logs_metadata_from_request_headers(headers: dict) -> dict | None: 

1302 """ 

1303 Get the `spend_logs_metadata` from the request headers. 

1304 """ 

1305 from litellm.litellm_core_utils.safe_json_loads import safe_json_loads 

1306 

1307 spend_logs_metadata_header: Final = headers.get("x-litellm-spend-logs-metadata", None) 

1308 if spend_logs_metadata_header is not None: 1308 ↛ 1309line 1308 didn't jump to line 1309 because the condition on line 1308 was never true

1309 return safe_json_loads(spend_logs_metadata_header) 

1310 return None 

1311 

1312 @staticmethod 

1313 def _get_forwardable_headers( 

1314 headers: Headers | dict, 

1315 ): 

1316 """ 

1317 Get the headers that should be forwarded to the LLM Provider. 

1318 

1319 Looks for any `x-` headers and sends them to the LLM Provider. 

1320 

1321 [07/09/2025] - Support 'anthropic-beta' header as well. 

1322 """ 

1323 forwarded_headers: Final = {} 

1324 for header, value in headers.items(): 

1325 if ( 

1326 header.lower().startswith("x-") 

1327 and not header.lower().startswith("x-stainless") 

1328 or header.lower().startswith("anthropic-beta") 

1329 ): # causes openai sdk to fail 

1330 forwarded_headers[header] = value 

1331 

1332 return forwarded_headers 

1333 

1334 @staticmethod 

1335 def _get_case_insensitive_header(headers: dict, key: str) -> str | None: 

1336 """ 

1337 Get a case-insensitive header from the headers dictionary. 

1338 """ 

1339 for header, value in headers.items(): 

1340 if header.lower() == key.lower(): 

1341 return value 

1342 return None 

1343 

1344 @staticmethod 

1345 def add_internal_user_from_user_mapping( 

1346 general_settings: dict | None, 

1347 user_api_key_dict: UserAPIKeyAuth, 

1348 headers: dict, 

1349 ) -> UserAPIKeyAuth: 

1350 if general_settings is None: 1350 ↛ 1351line 1350 didn't jump to line 1351 because the condition on line 1350 was never true

1351 return user_api_key_dict 

1352 user_header_mapping: Final = general_settings.get("user_header_mappings") 

1353 if not user_header_mapping: 1353 ↛ 1355line 1353 didn't jump to line 1355 because the condition on line 1353 was always true

1354 return user_api_key_dict 

1355 header_name: Final = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(user_header_mapping) 

1356 if not header_name: 

1357 return user_api_key_dict 

1358 header_value: Final = LiteLLMProxyRequestSetup._get_case_insensitive_header(headers, header_name) 

1359 if header_value: 

1360 user_api_key_dict.user_id = header_value 

1361 return user_api_key_dict 

1362 return user_api_key_dict 

1363 

1364 @staticmethod 

1365 def get_user_from_headers(headers: dict, general_settings: dict | None = None) -> str | None: 

1366 """ 

1367 Get the user from the specified header if `general_settings.user_header_name` is set. 

1368 """ 

1369 if general_settings is None: 1369 ↛ 1370line 1369 didn't jump to line 1370 because the condition on line 1369 was never true

1370 return None 

1371 

1372 header_name: Final = general_settings.get("user_header_name") 

1373 if header_name is None or header_name == "": 1373 ↛ 1376line 1373 didn't jump to line 1376 because the condition on line 1373 was always true

1374 return None 

1375 

1376 if not isinstance(header_name, str): 

1377 raise TypeError(f"Expected user_header_name to be a str but got {type(header_name)}") 

1378 

1379 user: Final = LiteLLMProxyRequestSetup._get_case_insensitive_header(headers, header_name) 

1380 if user is not None: 

1381 verbose_logger.info('found user "%s" in header "%s"', user, header_name) 

1382 

1383 return user 

1384 

1385 @staticmethod 

1386 def get_openai_org_id_from_headers(headers: dict, general_settings: dict | None = None) -> str | None: 

1387 """ 

1388 Get the OpenAI Org ID from the headers. 

1389 """ 

1390 if general_settings is not None and general_settings.get("forward_openai_org_id") is not True: 1390 ↛ 1392line 1390 didn't jump to line 1392 because the condition on line 1390 was always true

1391 return None 

1392 for header, value in headers.items(): 

1393 if header.lower() == "openai-organization": 

1394 verbose_logger.info("found openai org id: %s, sending to llm", value) 

1395 return value 

1396 return None 

1397 

1398 @staticmethod 

1399 def add_headers_to_llm_call(headers: dict, user_api_key_dict: UserAPIKeyAuth) -> dict: 

1400 """ 

1401 Add headers to the LLM call 

1402 

1403 - Checks request headers for forwardable headers 

1404 - Checks if user information should be added to the headers 

1405 """ 

1406 

1407 returned_headers: Final = LiteLLMProxyRequestSetup._get_forwardable_headers(headers) 

1408 

1409 if litellm.add_user_information_to_llm_headers is True: 

1410 litellm_logging_metadata_headers: Final = LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key( 

1411 user_api_key_dict=user_api_key_dict 

1412 ) 

1413 for k, v in litellm_logging_metadata_headers.items(): 

1414 if v is None: 

1415 continue 

1416 # httpx requires header values to be str or bytes; coerce numbers/bools 

1417 # to str and JSON-encode dict/list (e.g. user_api_key_spend is float, 

1418 # user_api_key_auth_metadata is dict). See #27458. 

1419 if isinstance(v, (dict, list)): 

1420 returned_headers[f"x-litellm-{k}"] = json.dumps(v) 

1421 elif isinstance(v, (str, bytes)): 

1422 returned_headers[f"x-litellm-{k}"] = v 

1423 else: 

1424 returned_headers[f"x-litellm-{k}"] = str(v) 

1425 

1426 return returned_headers 

1427 

1428 @staticmethod 

1429 def add_headers_to_llm_call_by_model_group(data: dict, headers: dict, user_api_key_dict: UserAPIKeyAuth) -> dict: 

1430 """ 

1431 Add headers to the LLM call by model group 

1432 """ 

1433 from litellm.proxy.auth.auth_checks import _check_model_access_helper 

1434 from litellm.proxy.proxy_server import llm_router 

1435 

1436 data_model: Final = data.get("model") 

1437 

1438 if ( 1438 ↛ 1450line 1438 didn't jump to line 1450 because the condition on line 1438 was never true

1439 data_model is not None 

1440 and litellm.model_group_settings is not None 

1441 and litellm.model_group_settings.forward_client_headers_to_llm_api is not None 

1442 and _check_model_access_helper( 

1443 model=data_model, 

1444 llm_router=llm_router, 

1445 models=litellm.model_group_settings.forward_client_headers_to_llm_api, 

1446 team_model_aliases=user_api_key_dict.team_model_aliases, 

1447 team_id=user_api_key_dict.team_id, 

1448 ) # handles aliases, wildcards, etc. 

1449 ): 

1450 _headers: Final = LiteLLMProxyRequestSetup.add_headers_to_llm_call(headers, user_api_key_dict) 

1451 if _headers != {}: 

1452 data["headers"] = _headers 

1453 return data 

1454 

1455 @staticmethod 

1456 def get_internal_user_header_from_mapping(user_header_mapping) -> str | None: 

1457 if not user_header_mapping: 

1458 return None 

1459 items: Final = user_header_mapping if isinstance(user_header_mapping, list) else [user_header_mapping] 

1460 for item in items: 

1461 if not isinstance(item, dict): 

1462 continue 

1463 role = item.get("litellm_user_role") 

1464 header_name = item.get("header_name") 

1465 if role is None or not header_name: 

1466 continue 

1467 if str(role).lower() == str(LitellmUserRoles.INTERNAL_USER).lower(): 

1468 return header_name 

1469 return None 

1470 

1471 @staticmethod 

1472 def add_litellm_data_for_backend_llm_call( 

1473 *, 

1474 headers: dict, 

1475 request_data: Mapping[str, object], 

1476 user_api_key_dict: UserAPIKeyAuth, 

1477 general_settings: dict[str, Any] | None = None, 

1478 ) -> LitellmDataForBackendLLMCall: 

1479 """ 

1480 - Adds user from headers 

1481 - Adds forwardable headers 

1482 - Adds org id 

1483 """ 

1484 data: Final = LitellmDataForBackendLLMCall() 

1485 

1486 if general_settings and general_settings.get("forward_client_headers_to_llm_api") is True: 1486 ↛ 1487line 1486 didn't jump to line 1487 because the condition on line 1486 was never true

1487 _headers: Final = LiteLLMProxyRequestSetup.add_headers_to_llm_call(headers, user_api_key_dict) 

1488 if _headers != {}: 

1489 data["headers"] = _headers 

1490 _organization: Final = LiteLLMProxyRequestSetup.get_openai_org_id_from_headers(headers, general_settings) 

1491 if _organization is not None: 1491 ↛ 1492line 1491 didn't jump to line 1492 because the condition on line 1491 was never true

1492 data["organization"] = _organization 

1493 

1494 header_timeout: Final = LiteLLMProxyRequestSetup._get_timeout_from_request(headers) 

1495 if header_timeout is not None: 1495 ↛ 1496line 1495 didn't jump to line 1496 because the condition on line 1495 was never true

1496 data["timeout"] = header_timeout 

1497 

1498 header_stream_timeout: Final = LiteLLMProxyRequestSetup._get_stream_timeout_from_request(headers) 

1499 if header_stream_timeout is not None: 1499 ↛ 1500line 1499 didn't jump to line 1500 because the condition on line 1499 was never true

1500 data["stream_timeout"] = header_stream_timeout 

1501 

1502 # Router._get_timeout resolves the effective per-attempt timeout from any of 

1503 # kwargs["timeout"], kwargs["request_timeout"], or kwargs["stream_timeout"], and a 

1504 # caller can supply any of those via the request body as well as the headers above. 

1505 # A deliberately tiny value can force a 408 on every deployment in a fallback chain, 

1506 # so this marker (never trusted verbatim from the client; stripped above) must cover 

1507 # every source cooldown_handlers._trigger_cooldown_for_failed_deployment needs to 

1508 # distinguish from a real deployment health signal. 

1509 if ( 

1510 header_timeout is not None 

1511 or header_stream_timeout is not None 

1512 or request_data.get("timeout") is not None 

1513 or request_data.get("request_timeout") is not None 

1514 or request_data.get("stream_timeout") is not None 

1515 ): 

1516 data["client_side_timeout"] = True 

1517 

1518 num_retries: Final = LiteLLMProxyRequestSetup._get_num_retries_from_request(headers) 

1519 if num_retries is not None: 1519 ↛ 1520line 1519 didn't jump to line 1520 because the condition on line 1519 was never true

1520 data["num_retries"] = num_retries 

1521 

1522 keepalive_seconds: Final = LiteLLMProxyRequestSetup._get_keepalive_seconds_from_request(headers) 

1523 if keepalive_seconds is not None: 1523 ↛ 1524line 1523 didn't jump to line 1524 because the condition on line 1523 was never true

1524 data["keepalive_seconds"] = keepalive_seconds 

1525 

1526 return data 

1527 

1528 @staticmethod 

1529 def add_litellm_metadata_from_request_headers( 

1530 headers: dict, 

1531 data: dict, 

1532 _metadata_variable_name: str, 

1533 ) -> dict: 

1534 """ 

1535 Add litellm metadata from request headers 

1536 

1537 Relevant issue: https://github.com/BerriAI/litellm/issues/14008 

1538 """ 

1539 from litellm.proxy._types import LitellmMetadataFromRequestHeaders 

1540 

1541 metadata_from_headers: Final = LitellmMetadataFromRequestHeaders() 

1542 spend_logs_metadata: Final = LiteLLMProxyRequestSetup._get_spend_logs_metadata_from_request_headers(headers) 

1543 if spend_logs_metadata is not None: 1543 ↛ 1544line 1543 didn't jump to line 1544 because the condition on line 1543 was never true

1544 metadata_from_headers["spend_logs_metadata"] = spend_logs_metadata 

1545 

1546 ######################################################################################### 

1547 # Finally update the requests metadata with the `metadata_from_headers` 

1548 ######################################################################################### 

1549 

1550 agent_id_from_header: Final = headers.get("x-litellm-agent-id") 

1551 # Explicit litellm headers take precedence; fall back to any x-*-session-id header. 

1552 chain_id: Final = get_chain_id_from_headers(dict(headers)) 

1553 

1554 if agent_id_from_header: 1554 ↛ 1555line 1554 didn't jump to line 1555 because the condition on line 1554 was never true

1555 metadata_from_headers["agent_id"] = agent_id_from_header 

1556 verbose_proxy_logger.debug("Extracted agent_id from header: %s", agent_id_from_header) 

1557 

1558 if chain_id: 1558 ↛ 1559line 1558 didn't jump to line 1559 because the condition on line 1558 was never true

1559 metadata_from_headers["trace_id"] = chain_id 

1560 metadata_from_headers["session_id"] = chain_id 

1561 data["litellm_session_id"] = chain_id 

1562 data["litellm_trace_id"] = chain_id 

1563 verbose_proxy_logger.debug("Extracted chain_id from header (trace-id/session-id): %s", chain_id) 

1564 else: 

1565 body_metadata: Final = data.get("metadata") 

1566 session_id: Final = _get_anthropic_session_id_from_metadata(body_metadata) 

1567 if session_id: 1567 ↛ 1568line 1567 didn't jump to line 1568 because the condition on line 1567 was never true

1568 metadata_from_headers["session_id"] = session_id 

1569 data["litellm_session_id"] = session_id 

1570 if isinstance(body_metadata, dict) and isinstance(body_metadata.get("user_id"), dict): 

1571 body_metadata["user_id"] = session_id 

1572 verbose_proxy_logger.debug("Extracted session_id from Anthropic metadata.user_id") 

1573 

1574 # Last-resort fallback: the W3C standards for trace/session propagation 

1575 # (https://www.w3.org/TR/trace-context/, https://www.w3.org/TR/baggage/). 

1576 # Lower priority than everything above - only fires when neither the 

1577 # explicit litellm headers nor the Anthropic-metadata path found 

1578 # anything - but lets a caller's existing traceparent/baggage headers 

1579 # (from real OTel instrumentation) correlate with litellm's own logs 

1580 # instead of generating an unrelated trace_id. 

1581 normalized_headers: Final = MappingProxyType({k.lower(): v for k, v in headers.items() if isinstance(k, str)}) 

1582 if "litellm_trace_id" not in data: 1582 ↛ 1592line 1582 didn't jump to line 1592 because the condition on line 1582 was always true

1583 traceparent: Final = normalized_headers.get("traceparent") 

1584 if isinstance(traceparent, str): 1584 ↛ 1585line 1584 didn't jump to line 1585 because the condition on line 1584 was never true

1585 trace_id_from_traceparent: Final = _trace_id_from_traceparent(traceparent) 

1586 if trace_id_from_traceparent: 

1587 metadata_from_headers["trace_id"] = trace_id_from_traceparent 

1588 data["litellm_trace_id"] = trace_id_from_traceparent # rebind-ok: data is an out-param 

1589 verbose_proxy_logger.debug( 

1590 "Extracted trace_id from W3C traceparent header: %s", trace_id_from_traceparent 

1591 ) 

1592 if "litellm_session_id" not in data: 1592 ↛ 1601line 1592 didn't jump to line 1601 because the condition on line 1592 was always true

1593 baggage: Final = normalized_headers.get("baggage") 

1594 if isinstance(baggage, str): 1594 ↛ 1595line 1594 didn't jump to line 1595 because the condition on line 1594 was never true

1595 session_id_from_baggage: Final = _session_id_from_baggage(baggage) 

1596 if session_id_from_baggage: 

1597 metadata_from_headers["session_id"] = session_id_from_baggage 

1598 data["litellm_session_id"] = session_id_from_baggage # rebind-ok: data is an out-param 

1599 verbose_proxy_logger.debug("Extracted session_id from W3C baggage header") 

1600 

1601 if isinstance(data[_metadata_variable_name], dict): 1601 ↛ 1603line 1601 didn't jump to line 1603 because the condition on line 1601 was always true

1602 data[_metadata_variable_name].update(metadata_from_headers) 

1603 return data 

1604 

1605 @staticmethod 

1606 def get_logged_api_key(user_api_key_dict: UserAPIKeyAuth) -> str | None: 

1607 if user_api_key_dict.is_session_token and user_api_key_dict.key_alias: 1607 ↛ 1608line 1607 didn't jump to line 1608 because the condition on line 1607 was never true

1608 return user_api_key_dict.key_alias 

1609 return user_api_key_dict.api_key 

1610 

1611 @staticmethod 

1612 def get_sanitized_user_information_from_key( 

1613 user_api_key_dict: UserAPIKeyAuth, 

1614 ) -> StandardLoggingUserAPIKeyMetadata: 

1615 stripped_metadata: Final = strip_callback_config(user_api_key_dict.metadata) 

1616 auth_metadata: Final = cast("dict[str, str] | None", stripped_metadata) # cast-ok: metadata is free-form JSON 

1617 user_api_key_logged_metadata: Final = StandardLoggingUserAPIKeyMetadata( 

1618 user_api_key_hash=LiteLLMProxyRequestSetup.get_logged_api_key(user_api_key_dict), 

1619 user_api_key_alias=user_api_key_dict.key_alias, 

1620 user_api_key_spend=user_api_key_dict.spend, 

1621 user_api_key_max_budget=user_api_key_dict.max_budget, 

1622 user_api_key_user_spend=user_api_key_dict.user_spend, 

1623 user_api_key_user_max_budget=user_api_key_dict.user_max_budget, 

1624 user_api_key_team_spend=user_api_key_dict.team_spend, 

1625 user_api_key_team_max_budget=user_api_key_dict.team_max_budget, 

1626 user_api_key_team_id=user_api_key_dict.team_id, 

1627 user_api_key_project_id=user_api_key_dict.project_id, 

1628 user_api_key_project_alias=user_api_key_dict.project_alias, 

1629 user_api_key_user_id=user_api_key_dict.user_id, 

1630 user_api_key_org_id=user_api_key_dict.org_id, 

1631 user_api_key_org_alias=user_api_key_dict.organization_alias, 

1632 user_api_key_team_alias=user_api_key_dict.team_alias, 

1633 user_api_key_end_user_id=user_api_key_dict.end_user_id, 

1634 user_api_key_user_email=user_api_key_dict.user_email, 

1635 user_api_key_request_route=user_api_key_dict.request_route, 

1636 user_api_key_budget_reset_at=( 

1637 user_api_key_dict.budget_reset_at.isoformat() if user_api_key_dict.budget_reset_at else None 

1638 ), 

1639 user_api_key_auth_metadata=auth_metadata, 

1640 ) 

1641 return user_api_key_logged_metadata 

1642 

1643 @staticmethod 

1644 def add_user_api_key_auth_to_request_metadata( 

1645 data: dict, 

1646 user_api_key_dict: UserAPIKeyAuth, 

1647 _metadata_variable_name: str, 

1648 ) -> dict: 

1649 """ 

1650 Adds the `UserAPIKeyAuth` object to the request metadata. 

1651 """ 

1652 user_api_key_logged_metadata: Final = LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key( 

1653 user_api_key_dict=user_api_key_dict 

1654 ) 

1655 data[_metadata_variable_name].update(user_api_key_logged_metadata) 

1656 data[_metadata_variable_name]["user_api_key"] = LiteLLMProxyRequestSetup.get_logged_api_key(user_api_key_dict) 

1657 

1658 # Key-owned agent_id for spend attribution; keep existing (e.g. from header) if key has none 

1659 _key_agent_id: Final = getattr(user_api_key_dict, "agent_id", None) 

1660 _existing_agent_id: Final = data[_metadata_variable_name].get("agent_id") 

1661 _resolved_agent_id: Final = _key_agent_id or _existing_agent_id 

1662 data[_metadata_variable_name]["agent_id"] = _resolved_agent_id 

1663 

1664 data[_metadata_variable_name]["user_api_end_user_max_budget"] = getattr( 

1665 user_api_key_dict, "end_user_max_budget", None 

1666 ) 

1667 if user_api_key_dict.budget_reservation is not None: 1667 ↛ 1668line 1667 didn't jump to line 1668 because the condition on line 1667 was never true

1668 data[_metadata_variable_name]["user_api_key_budget_reservation"] = user_api_key_dict.budget_reservation 

1669 if user_api_key_dict.matched_model_access_groups: 1669 ↛ 1670line 1669 didn't jump to line 1670 because the condition on line 1669 was never true

1670 data[_metadata_variable_name][MODEL_ACCESS_GROUP_METADATA_KEY] = ( 

1671 user_api_key_dict.matched_model_access_groups 

1672 ) 

1673 # UserAPIKeyAuth object for MCP server access control 

1674 data[_metadata_variable_name]["user_api_key_auth"] = user_api_key_dict.model_copy( 

1675 update={ 

1676 "metadata": strip_callback_config(user_api_key_dict.metadata), 

1677 "team_metadata": strip_callback_config(user_api_key_dict.team_metadata), 

1678 "project_metadata": strip_callback_config(user_api_key_dict.project_metadata), 

1679 "organization_metadata": strip_callback_config(user_api_key_dict.organization_metadata), 

1680 } 

1681 ) 

1682 return data 

1683 

1684 @staticmethod 

1685 def add_management_endpoint_metadata_to_request_metadata( 

1686 data: dict, 

1687 management_endpoint_metadata: dict, 

1688 _metadata_variable_name: str, 

1689 ) -> dict: 

1690 """ 

1691 Adds the `UserAPIKeyAuth` metadata to the request metadata. 

1692 

1693 ignore any sensitive fields like logging, api_key, etc. 

1694 """ 

1695 if _metadata_variable_name not in data: 1695 ↛ 1696line 1695 didn't jump to line 1696 because the condition on line 1695 was never true

1696 return data 

1697 from litellm.proxy._types import ( 

1698 LiteLLM_ManagementEndpoint_MetadataFields, 

1699 LiteLLM_ManagementEndpoint_MetadataFields_Premium, 

1700 ) 

1701 

1702 # ignore any special fields 

1703 added_metadata: Final = { 

1704 k: v 

1705 for k, v in (strip_callback_config(management_endpoint_metadata) or {}).items() 

1706 if k not in (LiteLLM_ManagementEndpoint_MetadataFields_Premium + LiteLLM_ManagementEndpoint_MetadataFields) 

1707 } 

1708 if data[_metadata_variable_name].get("user_api_key_auth_metadata") is None: 1708 ↛ 1709line 1708 didn't jump to line 1709 because the condition on line 1708 was never true

1709 data[_metadata_variable_name]["user_api_key_auth_metadata"] = {} 

1710 data[_metadata_variable_name]["user_api_key_auth_metadata"].update(added_metadata) 

1711 return data 

1712 

1713 @staticmethod 

1714 def add_key_level_controls(key_metadata: dict | None, data: dict, _metadata_variable_name: str): 

1715 if key_metadata is None: 1715 ↛ 1716line 1715 didn't jump to line 1716 because the condition on line 1715 was never true

1716 return data 

1717 if "cache" in key_metadata: 1717 ↛ 1718line 1717 didn't jump to line 1718 because the condition on line 1717 was never true

1718 data["cache"] = {} 

1719 if isinstance(key_metadata["cache"], dict): 

1720 for k, v in key_metadata["cache"].items(): 

1721 if k in SupportedCacheControls: 

1722 data["cache"][k] = v 

1723 

1724 ## KEY-LEVEL SPEND LOGS / TAGS 

1725 if "tags" in key_metadata and key_metadata["tags"] is not None: 1725 ↛ 1726line 1725 didn't jump to line 1726 because the condition on line 1725 was never true

1726 data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( 

1727 request_tags=data[_metadata_variable_name].get("tags"), 

1728 tags_to_add=key_metadata["tags"], 

1729 ) 

1730 if "disable_global_guardrails" in key_metadata and isinstance(key_metadata["disable_global_guardrails"], bool): 1730 ↛ 1731line 1730 didn't jump to line 1731 because the condition on line 1730 was never true

1731 data[_metadata_variable_name]["disable_global_guardrails"] = key_metadata["disable_global_guardrails"] 

1732 if "spend_logs_metadata" in key_metadata and isinstance(key_metadata["spend_logs_metadata"], dict): 1732 ↛ 1733line 1732 didn't jump to line 1733 because the condition on line 1732 was never true

1733 if "spend_logs_metadata" in data[_metadata_variable_name] and isinstance( 

1734 data[_metadata_variable_name]["spend_logs_metadata"], dict 

1735 ): 

1736 for key, value in key_metadata["spend_logs_metadata"].items(): 

1737 if ( 

1738 key not in data[_metadata_variable_name]["spend_logs_metadata"] 

1739 ): # don't override k-v pair sent by request (user request) 

1740 data[_metadata_variable_name]["spend_logs_metadata"][key] = value 

1741 else: 

1742 data[_metadata_variable_name]["spend_logs_metadata"] = key_metadata["spend_logs_metadata"] 

1743 

1744 ## KEY-LEVEL DISABLE FALLBACKS 

1745 if "disable_fallbacks" in key_metadata and isinstance(key_metadata["disable_fallbacks"], bool): 1745 ↛ 1746line 1745 didn't jump to line 1746 because the condition on line 1745 was never true

1746 data["disable_fallbacks"] = key_metadata["disable_fallbacks"] 

1747 

1748 if isinstance(key_metadata.get("enable_prompt_caching"), bool): 1748 ↛ 1749line 1748 didn't jump to line 1749 because the condition on line 1748 was never true

1749 data["enable_prompt_caching"] = key_metadata["enable_prompt_caching"] # rebind-ok: data is an out-param 

1750 

1751 ## KEY-LEVEL METADATA 

1752 data = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata( 

1753 data=data, 

1754 management_endpoint_metadata=key_metadata, 

1755 _metadata_variable_name=_metadata_variable_name, 

1756 ) 

1757 return data 

1758 

1759 @staticmethod 

1760 def _merge_tags(request_tags: list | None, tags_to_add: list | None) -> list: 

1761 """ 

1762 Helper function to merge two lists of tags, ensuring no duplicates. 

1763 

1764 Args: 

1765 request_tags (Optional[list]): List of tags from the original request 

1766 tags_to_add (Optional[list]): List of tags to add 

1767 

1768 Returns: 

1769 list: Combined list of unique tags 

1770 """ 

1771 final_tags: Final = [] 

1772 

1773 if request_tags and isinstance(request_tags, list): 

1774 final_tags.extend(request_tags) 

1775 

1776 if tags_to_add and isinstance(tags_to_add, list): 

1777 for tag in tags_to_add: 

1778 if tag not in final_tags: 

1779 final_tags.append(tag) 

1780 

1781 return final_tags 

1782 

1783 @staticmethod 

1784 def add_team_based_callbacks_from_config( 

1785 team_id: str, 

1786 proxy_config: ProxyConfig, 

1787 ) -> TeamCallbackMetadata | None: 

1788 """ 

1789 Add team-based callbacks from the config 

1790 """ 

1791 team_config: Final = proxy_config.load_team_config(team_id=team_id) 

1792 if not isinstance(team_config, dict) or len(team_config) == 0: 

1793 return None 

1794 

1795 callback_vars_dict = {**team_config.get("callback_vars", team_config)} 

1796 callback_vars_dict.pop("team_id", None) 

1797 callback_vars_dict.pop("success_callback", None) 

1798 callback_vars_dict.pop("failure_callback", None) 

1799 callback_vars_dict = { 

1800 key: ( 

1801 litellm.utils.get_secret(value, default_value=value) or value if isinstance(value, str) else str(value) 

1802 ) 

1803 for key, value in callback_vars_dict.items() 

1804 } 

1805 

1806 return TeamCallbackMetadata( 

1807 success_callback=team_config.get("success_callback", None), 

1808 failure_callback=team_config.get("failure_callback", None), 

1809 callback_vars=callback_vars_dict, 

1810 ) 

1811 

1812 @staticmethod 

1813 def add_request_tag_to_metadata( 

1814 llm_router: Router | None, 

1815 headers: dict, 

1816 data: dict, 

1817 ) -> list[str] | None: 

1818 tags = None 

1819 

1820 # Check request headers for tags 

1821 if "x-litellm-tags" in headers: 1821 ↛ 1822line 1821 didn't jump to line 1822 because the condition on line 1821 was never true

1822 if isinstance(headers["x-litellm-tags"], str): 

1823 _tags: Final = headers["x-litellm-tags"].split(",") 

1824 tags = [tag.strip() for tag in _tags] 

1825 elif isinstance(headers["x-litellm-tags"], list): 

1826 tags = headers["x-litellm-tags"] 

1827 # Check request body for tags 

1828 if "tags" in data and isinstance(data["tags"], list): 1828 ↛ 1829line 1828 didn't jump to line 1829 because the condition on line 1828 was never true

1829 tags = data["tags"] 

1830 

1831 return tags 

1832 

1833 @staticmethod 

1834 def pre_seed_litellm_metadata_for_route( 

1835 request_data: dict, 

1836 route: str, 

1837 ) -> None: 

1838 """Pre-seed ``litellm_metadata`` for routes that track tags there. 

1839 

1840 Routes in ``LITELLM_METADATA_ROUTES`` (e.g. Bedrock, ``/v1/messages``, 

1841 responses, batches, files) store request-scoped tag metadata in 

1842 ``litellm_metadata`` rather than the provider-facing ``metadata`` 

1843 field. ``get_metadata_variable_name_from_kwargs`` picks the target 

1844 based on whether ``litellm_metadata`` is present, so it must be 

1845 seeded BEFORE any tag merge runs; otherwise header tags from 

1846 ``apply_client_tag_policy_pre_auth`` land in ``metadata`` while 

1847 key tags from ``apply_key_tags_pre_auth`` and the read in 

1848 ``_tag_max_budget_check`` resolve to ``litellm_metadata``, leaving 

1849 header tags invisible to per-tag budget enforcement. 

1850 """ 

1851 if any(metadata_route in route for metadata_route in LITELLM_METADATA_ROUTES): 

1852 request_data.setdefault("litellm_metadata", {}) 

1853 

1854 @staticmethod 

1855 def apply_key_tags_pre_auth( 

1856 request_data: dict, 

1857 user_api_key_dict: UserAPIKeyAuth, 

1858 ) -> None: 

1859 """Merge key metadata tags into request_data before _tag_max_budget_check.""" 

1860 key_metadata: Final = user_api_key_dict.metadata 

1861 if not key_metadata: 

1862 return 

1863 

1864 key_tags: Final = key_metadata.get("tags") 

1865 if not key_tags or not isinstance(key_tags, list): 1865 ↛ 1868line 1865 didn't jump to line 1868 because the condition on line 1865 was always true

1866 return 

1867 

1868 _metadata_variable_name: Final = get_metadata_variable_name_from_kwargs(request_data) 

1869 metadata: Final = _normalized_metadata_slot(request_data, _metadata_variable_name) 

1870 

1871 existing_tags: Final = metadata.get("tags") 

1872 metadata["tags"] = LiteLLMProxyRequestSetup._merge_tags( 

1873 request_tags=existing_tags if isinstance(existing_tags, list) else None, 

1874 tags_to_add=key_tags, 

1875 ) 

1876 

1877 @staticmethod 

1878 def apply_client_tag_policy_pre_auth( 

1879 request: Request, 

1880 request_data: dict, 

1881 user_api_key_dict: UserAPIKeyAuth, 

1882 ) -> None: 

1883 """ 

1884 Merge ``x-litellm-tags`` header tags into ``request_data`` BEFORE 

1885 auth budget gates run, so ``_tag_max_budget_check`` (which only 

1886 inspects ``request_data``) sees them. Without this, header-tagged 

1887 requests silently bypass per-tag budget enforcement. 

1888 

1889 Why: ``add_litellm_data_to_request`` runs the equivalent merge 

1890 post-auth, after ``_tag_max_budget_check`` has already executed. 

1891 Header-supplied tags merged there are invisible to that check. 

1892 Running the merge here closes that gap; the post-auth merge in 

1893 ``add_litellm_data_to_request`` remains as defense-in-depth. 

1894 

1895 How to apply: invoked from the auth chain just before 

1896 ``common_checks``. Mutates ``request_data`` in place; idempotent 

1897 when followed by ``add_litellm_data_to_request``. 

1898 """ 

1899 # No allow_client_tags opt-in: caller-supplied tags always flow 

1900 # into metadata.tags (see add_litellm_data_to_request). The pre-auth 

1901 # merge mirrors that so _tag_max_budget_check sees the same tags. 

1902 headers: Final = _safe_get_request_headers(request=request) 

1903 raw_header_tags: Final = headers.get("x-litellm-tags") 

1904 if not raw_header_tags: 1904 ↛ 1907line 1904 didn't jump to line 1907 because the condition on line 1904 was always true

1905 return 

1906 

1907 if isinstance(raw_header_tags, str): 

1908 header_tags: list[str] = [t.strip() for t in raw_header_tags.split(",") if t.strip()] 

1909 elif isinstance(raw_header_tags, list): 

1910 header_tags = [t for t in raw_header_tags if isinstance(t, str) and t] 

1911 else: 

1912 return 

1913 

1914 if not header_tags: 

1915 return 

1916 

1917 # Match the metadata key that get_tags_from_request_body will read 

1918 # from (litellm_metadata vs metadata) so the merged tags are visible 

1919 # to _tag_max_budget_check. 

1920 _metadata_variable_name: Final = get_metadata_variable_name_from_kwargs(request_data) 

1921 metadata: Final = _normalized_metadata_slot(request_data, _metadata_variable_name) 

1922 

1923 existing_tags: Final = metadata.get("tags") 

1924 metadata["tags"] = LiteLLMProxyRequestSetup._merge_tags( 

1925 request_tags=existing_tags if isinstance(existing_tags, list) else None, 

1926 tags_to_add=header_tags, 

1927 ) 

1928 

1929 

1930def refresh_proxy_server_request_body_snapshot( 

1931 data: MutableMapping[str, object], 

1932 *, 

1933 guardrails_applied: bool = False, 

1934) -> None: 

1935 """ 

1936 Re-snapshot ``data["proxy_server_request"]["body"]`` from the current state of ``data``. 

1937 

1938 ``add_litellm_data_to_request`` takes the initial snapshot before guardrails 

1939 (pre_call_hook) run. A guardrail that masks PII/PCI in place (e.g. Presidio) 

1940 mutates ``data`` afterward, so callers that persist ``proxy_server_request.body`` 

1941 for audit/spend-tracking purposes must call this again post-guardrail, or the 

1942 persisted body silently bypasses whatever masking the guardrail applied. 

1943 

1944 By the time a caller refreshes post-guardrail, ``litellm.utils.function_setup`` 

1945 has already stamped ``data["litellm_logging_obj"]`` with a live (non-serializable) 

1946 ``Logging`` instance, so it must be excluded here the same way ``secret_fields`` 

1947 and ``proxy_server_request`` are. 

1948 """ 

1949 from litellm.integrations.shadow_eval_logger import GuardrailRequestSnapshot 

1950 from litellm.litellm_core_utils.litellm_logging import Logging 

1951 

1952 logging_obj: Final = data.get("litellm_logging_obj") 

1953 if isinstance(logging_obj, Logging): 

1954 logging_obj.shadow_eval_request_snapshot = None 

1955 proxy_server_request: Final = data.get("proxy_server_request") 

1956 if not isinstance(proxy_server_request, dict): 1956 ↛ 1957line 1956 didn't jump to line 1957 because the condition on line 1956 was never true

1957 return 

1958 _body_snapshot_exclude: Final = ( 

1959 frozenset({"secret_fields", "proxy_server_request", "litellm_logging_obj"}) | _TRANSPORT_ONLY_CREDENTIAL_KEYS 

1960 ) 

1961 body: Final = { # mutable-ok: audit JSON serialization requires a dict with shared nested messages 

1962 k: v for k, v in data.items() if k not in _body_snapshot_exclude 

1963 } 

1964 proxy_server_request["body"] = body 

1965 if guardrails_applied and isinstance(logging_obj, Logging): 

1966 metadata: Final = data.get(get_metadata_variable_name_from_kwargs(data)) 

1967 logging_obj.shadow_eval_request_snapshot = GuardrailRequestSnapshot.capture( 

1968 body, metadata if isinstance(metadata, Mapping) else MappingProxyType({}) 

1969 ) 

1970 

1971 

1972async def add_litellm_data_to_request( 

1973 data: dict, 

1974 request: Request, 

1975 user_api_key_dict: UserAPIKeyAuth, 

1976 proxy_config: ProxyConfig, 

1977 general_settings: dict[str, Any] | None = None, 

1978 version: str | None = None, 

1979): 

1980 """ 

1981 Adds LiteLLM-specific data to the request. 

1982 

1983 Args: 

1984 data (dict): The data dictionary to be modified. 

1985 request (Request): The incoming request. 

1986 user_api_key_dict (UserAPIKeyAuth): The user API key dictionary. 

1987 general_settings (Optional[Dict[str, Any]], optional): General settings. Defaults to None. 

1988 version (Optional[str], optional): Version. Defaults to None. 

1989 

1990 Returns: 

1991 dict: The modified data dictionary. 

1992 

1993 """ 

1994 

1995 from litellm.proxy.proxy_server import llm_router, premium_user 

1996 from litellm.types.proxy.litellm_pre_call_utils import RedactedDict, SecretFields 

1997 

1998 # Strip internal-only keys from user input before the proxy sets its own. 

1999 # These keys are injected by the proxy itself below — user-supplied values 

2000 # must not be trusted. 

2001 _allow_client_mock_response: Final = _key_or_team_allows_client_mock_response(user_api_key_dict) 

2002 _allow_client_message_redaction_opt_out = _key_or_team_allows_client_message_redaction_opt_out(user_api_key_dict) 

2003 for _internal_key in _UNTRUSTED_ROOT_CONTROL_FIELDS: 

2004 if _allow_client_mock_response and _internal_key in _CLIENT_MOCK_CONTROL_FIELDS: 2004 ↛ 2005line 2004 didn't jump to line 2005 because the condition on line 2004 was never true

2005 continue 

2006 data.pop(_internal_key, None) 

2007 _reject_url_valued_destinations(data) 

2008 _raw_metadata_by_field: Final = { 

2009 _metadata_field: data.pop(_metadata_field) 

2010 for _metadata_field in ("metadata", "litellm_metadata") 

2011 if data.get(_metadata_field) is not None 

2012 } 

2013 for _metadata_field, _raw_metadata in _raw_metadata_by_field.items(): 

2014 data[_metadata_field] = _normalized_metadata_object(_metadata_field, _raw_metadata) 

2015 # Strip spoofable auth metadata from user-supplied metadata dict 

2016 _user_metadata = data.get("metadata") 

2017 if isinstance(_user_metadata, dict): 

2018 for _mk in list(_user_metadata.keys()): 

2019 if _mk.startswith("user_api_key_"): 

2020 del _user_metadata[_mk] 

2021 

2022 _raw_headers: Final[dict[str, str]] = RedactedDict(_safe_get_request_headers(request)) 

2023 

2024 forward_llm_auth = False 

2025 if general_settings: 2025 ↛ 2027line 2025 didn't jump to line 2027 because the condition on line 2025 was always true

2026 forward_llm_auth = general_settings.get("forward_llm_provider_auth_headers", False) 

2027 if not forward_llm_auth: 2027 ↛ 2031line 2027 didn't jump to line 2031 because the condition on line 2027 was always true

2028 forward_llm_auth = getattr(litellm, "forward_llm_provider_auth_headers", False) 

2029 # Determine which header was used for authentication 

2030 # This enables forwarding provider keys (e.g., x-api-key) when they weren't used for LiteLLM auth 

2031 authenticated_with_header = None 

2032 if "x-litellm-api-key" in request.headers: 

2033 # If x-litellm-api-key is present, it was used for auth 

2034 authenticated_with_header = "x-litellm-api-key" 

2035 elif "authorization" in request.headers: 2035 ↛ 2037line 2035 didn't jump to line 2037 because the condition on line 2035 was never true

2036 # Authorization header was used for auth 

2037 authenticated_with_header = "authorization" 

2038 else: 

2039 # x-api-key or another header was used for auth 

2040 authenticated_with_header = "x-api-key" 

2041 

2042 _headers: Final[dict[str, str]] = clean_headers( 

2043 request.headers, 

2044 litellm_key_header_name=( 

2045 general_settings.get("litellm_key_header_name") if general_settings is not None else None 

2046 ), 

2047 forward_llm_provider_auth_headers=forward_llm_auth, 

2048 authenticated_with_header=authenticated_with_header, 

2049 ) 

2050 _strip_untrusted_request_header_controls( 

2051 _headers, 

2052 allow_client_message_redaction_opt_out=_allow_client_message_redaction_opt_out, 

2053 ) 

2054 from litellm.proxy._experimental.mcp_server.utils import upstream_credential_headers 

2055 

2056 _mcp_credential_headers: Final = upstream_credential_headers(_headers) 

2057 _logging_safe_headers: Final = redact_credential_headers( 

2058 MappingProxyType( 

2059 {name: value for name, value in _headers.items() if name.lower() not in _mcp_credential_headers} 

2060 ) 

2061 ) 

2062 verbose_proxy_logger.debug("Request Headers: %s", _logging_safe_headers) 

2063 verbose_proxy_logger.debug("Raw Headers: %s", _raw_headers) 

2064 

2065 if forward_llm_auth and "x-api-key" in _headers: 2065 ↛ 2066line 2065 didn't jump to line 2066 because the condition on line 2065 was never true

2066 data["api_key"] = _headers["x-api-key"] 

2067 verbose_proxy_logger.debug( 

2068 "Setting client-provided x-api-key as api_key parameter (will override deployment key)" 

2069 ) 

2070 

2071 ########################################################## 

2072 # Init - Proxy Server Request 

2073 # we do this as soon as entering so we track the original request 

2074 ########################################################## 

2075 # Track arrival time for queue time metric. Prefer the timestamp stamped at 

2076 # the top of user_api_key_auth (request.state.litellm_received_at): by the 

2077 # time this function runs, auth has already completed, so time.time() here 

2078 # would silently exclude the entire auth phase from the queue-time window. 

2079 # Falls back to time.time() for callers that never went through 

2080 # user_api_key_auth. The body snapshot is filled in after the 

2081 # admin-injection strip below so the audit / spend-tracking consumers of 

2082 # proxy_server_request["body"] see the cleaned metadata rather than 

2083 # attacker-forged user_api_key_* fields. 

2084 _litellm_received_at: Final[datetime | None] = getattr(request.state, "litellm_received_at", None) 

2085 arrival_time: Final = _litellm_received_at.timestamp() if _litellm_received_at is not None else time.time() 

2086 data["proxy_server_request"] = { 

2087 "url": str(request.url), 

2088 "method": request.method, 

2089 "headers": _logging_safe_headers, 

2090 "body": None, # filled in post-strip; see below 

2091 "credential_fields": tuple(sorted(name for name in _TRANSPORT_ONLY_CREDENTIAL_KEYS if name in data)), 

2092 "arrival_time": arrival_time, # Track when request arrived at proxy 

2093 } 

2094 

2095 safe_add_api_version_from_query_params(data, request) 

2096 _metadata_variable_name: Final = _get_metadata_variable_name(request) 

2097 if data.get(_metadata_variable_name, None) is None: 

2098 data[_metadata_variable_name] = {} 

2099 

2100 data.update( 

2101 LiteLLMProxyRequestSetup.add_litellm_data_for_backend_llm_call( 

2102 headers=_headers, 

2103 request_data=data, 

2104 user_api_key_dict=user_api_key_dict, 

2105 general_settings=general_settings, 

2106 ) 

2107 ) 

2108 

2109 LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( 

2110 headers=_headers, 

2111 data=data, 

2112 _metadata_variable_name=_metadata_variable_name, 

2113 ) 

2114 add_otel_trace_id_to_request( 

2115 data=data, 

2116 _metadata_variable_name=_metadata_variable_name, 

2117 parent_otel_span=user_api_key_dict.parent_otel_span 

2118 if user_api_key_dict.parent_otel_span is not None 

2119 else getattr(request.state, "parent_otel_span", None), 

2120 ) 

2121 apply_missing_session_id_policy( 

2122 data=data, 

2123 _metadata_variable_name=_metadata_variable_name, 

2124 general_settings=general_settings, 

2125 request=request, 

2126 ) 

2127 

2128 # Expose request headers under the metadata field for guardrails (fixes #17477) 

2129 if _metadata_variable_name in data and isinstance(data[_metadata_variable_name], dict): 2129 ↛ 2133line 2129 didn't jump to line 2133 because the condition on line 2129 was always true

2130 data[_metadata_variable_name]["headers"] = _logging_safe_headers 

2131 

2132 # check for forwardable headers 

2133 data = LiteLLMProxyRequestSetup.add_headers_to_llm_call_by_model_group( 

2134 data=data, headers=_headers, user_api_key_dict=user_api_key_dict 

2135 ) 

2136 

2137 user_api_key_dict = LiteLLMProxyRequestSetup.add_internal_user_from_user_mapping( 

2138 general_settings, user_api_key_dict, _headers 

2139 ) 

2140 

2141 # Parse user info from headers (fallback to general_settings.user_header_name) 

2142 user: Final = LiteLLMProxyRequestSetup.get_user_from_headers(_headers, general_settings) 

2143 if user is not None: 2143 ↛ 2144line 2143 didn't jump to line 2144 because the condition on line 2143 was never true

2144 if user_api_key_dict.end_user_id is None: 

2145 user_api_key_dict.end_user_id = user 

2146 if "user" not in data: 

2147 data["user"] = user 

2148 

2149 if litellm.overwrite_user_with_key_hash is True: 2149 ↛ 2150line 2149 didn't jump to line 2150 because the condition on line 2149 was never true

2150 stampable_hash: Final = _stampable_key_hash(user_api_key_dict) 

2151 if stampable_hash is not None: 

2152 data["user"] = stampable_hash 

2153 

2154 data["secret_fields"] = SecretFields(raw_headers=_raw_headers) 

2155 

2156 ## Dynamic api version (Azure OpenAI endpoints) ## 

2157 try: 

2158 query_params: Final = request.query_params 

2159 # Convert query parameters to a dictionary (optional) 

2160 query_dict = dict(query_params) 

2161 except KeyError: 

2162 query_dict = {} 

2163 

2164 ## check for api version in query params 

2165 dynamic_api_version: Final[str | None] = query_dict.get("api-version") 

2166 

2167 if dynamic_api_version is not None: # only pass, if set 2167 ↛ 2168line 2167 didn't jump to line 2168 because the condition on line 2167 was never true

2168 data["api_version"] = dynamic_api_version 

2169 

2170 ## Forward any LLM API Provider specific headers in extra_headers 

2171 add_provider_specific_headers_to_request(data=data, headers=_headers) 

2172 

2173 ## Cache Controls 

2174 cache_control_header: Final = _headers.get("Cache-Control", None) 

2175 if cache_control_header: 2175 ↛ 2176line 2175 didn't jump to line 2176 because the condition on line 2175 was never true

2176 cache_dict: Final = parse_cache_control(cache_control_header) 

2177 data["ttl"] = cache_dict.get("s-maxage") 

2178 

2179 # requester_metadata is snapshotted AFTER the strip below so 

2180 # downstream consumers (e.g. PANW guardrail reading user_ip / 

2181 # profile_id) don't see attacker-injected admin slots preserved in 

2182 # the deepcopy. 

2183 

2184 # Strip internal pipeline state and admin-injection slots from user input. 

2185 # Runs AFTER the string-to-dict parse above so JSON-string metadata (sent 

2186 # via multipart/form-data or extra_body) cannot smuggle admin fields past 

2187 # the isinstance(dict) guard. 

2188 # 

2189 # The proxy populates a family of ``user_api_key_*`` fields below 

2190 # (user_api_key_metadata, user_api_key_user_id, user_api_key_alias, 

2191 # user_api_key_spend, user_api_key_team_metadata, …) into 

2192 # data[_metadata_variable_name]. Because the proxy only writes to ONE of 

2193 # the two metadata dicts, a caller pre-populating any of these keys on 

2194 # the OTHER metadata dict would have their forged values surface in 

2195 # guardrails, spend tracking, audit logs, and identity resolution. Strip 

2196 # by prefix so new ``user_api_key_*`` fields added in the future are 

2197 # covered without per-key maintenance. 

2198 for _meta_key in ("metadata", "litellm_metadata"): 

2199 _user_meta = data.get(_meta_key) 

2200 if isinstance(_user_meta, dict): 

2201 _strip_untrusted_request_header_controls( 

2202 _user_meta.get("headers"), 

2203 allow_client_message_redaction_opt_out=(_allow_client_message_redaction_opt_out), 

2204 ) 

2205 for _k in [ 

2206 k for k in _user_meta if k.startswith("user_api_key_") or k in _UNTRUSTED_METADATA_CONTROL_FIELDS 

2207 ]: 

2208 _user_meta.pop(_k, None) 

2209 

2210 # Strip pricing overrides AFTER the litellm_metadata string-to-dict parse 

2211 # above, for the same reason as the user_api_key_* strip — JSON-string 

2212 # metadata (sent via multipart/form-data or extra_body) wouldn't be a 

2213 # dict yet at the earlier strip point and the isinstance(dict) guard 

2214 # would silently skip the field. 

2215 if not _key_or_team_allows_client_pricing_override(user_api_key_dict): 2215 ↛ 2217line 2215 didn't jump to line 2217 because the condition on line 2215 was always true

2216 _strip_client_pricing_overrides(data) 

2217 _strip_router_reserved_metadata(data) 

2218 

2219 # Same reason as the strips above: runs after the metadata string-to-dict parse 

2220 # so JSON-string metadata cannot smuggle callback credentials past the dict guard. 

2221 _strip_client_callback_credentials(data) 

2222 

2223 if not _allow_client_message_redaction_opt_out and litellm.turn_off_message_logging is True: 2223 ↛ 2224line 2223 didn't jump to line 2224 because the condition on line 2223 was never true

2224 _strip_client_message_redaction_opt_out(data) 

2225 

2226 # Fill in the proxy_server_request body snapshot now that metadata has 

2227 # been parsed. Consumers (standard_logging_payload, lago, 

2228 # spend_tracking_utils, streaming_iterator) read `body` to audit the 

2229 # request; taking the snapshot here ensures they see cleaned metadata. 

2230 # 

2231 # Exclude: 

2232 # - secret_fields: contains raw_headers with Authorization tokens; must 

2233 # never be persisted in spend logs or any other audit trail. 

2234 # - proxy_server_request: already a key on `data` at this point (set 

2235 # earlier in this function); including it would make the snapshot 

2236 # self-reference — body.proxy_server_request.body would be the same 

2237 # dict as body, producing an infinite traversal loop for any consumer 

2238 # that walks the structure. 

2239 refresh_proxy_server_request_body_snapshot(data) 

2240 

2241 # Snapshot the requester-supplied metadata for downstream consumers. 

2242 # Taking the deepcopy after the user_api_key_* / _pipeline_managed_guardrails 

2243 # strip above prevents those proxy-internal slots — if a caller forged 

2244 # them — from leaking into requester_metadata where guardrails and audit 

2245 # paths may read from it. 

2246 if "metadata" in data and isinstance(data["metadata"], dict): 

2247 data[_metadata_variable_name]["requester_metadata"] = copy.deepcopy(data["metadata"]) 

2248 if _metadata_variable_name == "litellm_metadata": 2248 ↛ 2249line 2248 didn't jump to line 2249 because the condition on line 2248 was never true

2249 data[_metadata_variable_name].update( 

2250 _promoted_trace_control_fields( 

2251 requester_metadata=data[_metadata_variable_name]["requester_metadata"], 

2252 litellm_metadata=data[_metadata_variable_name], 

2253 ) 

2254 ) 

2255 

2256 # Merge litellm_metadata into the metadata variable (preserving existing 

2257 # values). Runs after the user_api_key_* / _pipeline_managed_guardrails 

2258 # strip above so those proxy-internal slots — if a caller forged them 

2259 # into litellm_metadata — cannot cross-contaminate the admin-authoritative 

2260 # metadata dict. 

2261 if "litellm_metadata" in data and isinstance(data["litellm_metadata"], dict): 

2262 for key, value in data["litellm_metadata"].items(): 

2263 if key not in data[_metadata_variable_name]: 2263 ↛ 2264line 2263 didn't jump to line 2264 because the condition on line 2263 was never true

2264 data[_metadata_variable_name][key] = value 

2265 if _metadata_variable_name == "metadata": 2265 ↛ 2266line 2265 didn't jump to line 2266 because the condition on line 2265 was never true

2266 data["metadata"]["tags"] = LiteLLMProxyRequestSetup._merge_tags( # pyright: ignore[reportPrivateUsage] # same-module helper, budget blocks the unsuppressed idiom sibling call sites use 

2267 request_tags=data["metadata"].get("tags"), 

2268 tags_to_add=data["litellm_metadata"].get("tags"), 

2269 ) 

2270 if _metadata_variable_name == "metadata": 

2271 data.pop("litellm_metadata", None) 

2272 

2273 data = LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata( 

2274 data=data, 

2275 user_api_key_dict=user_api_key_dict, 

2276 _metadata_variable_name=_metadata_variable_name, 

2277 ) 

2278 data[_metadata_variable_name]["litellm_api_version"] = version 

2279 

2280 if general_settings is not None: 2280 ↛ 2286line 2280 didn't jump to line 2286 because the condition on line 2280 was always true

2281 data[_metadata_variable_name]["global_max_parallel_requests"] = general_settings.get( 

2282 "global_max_parallel_requests", None 

2283 ) 

2284 

2285 ### KEY-LEVEL Controls 

2286 key_metadata: Final = user_api_key_dict.metadata 

2287 data = LiteLLMProxyRequestSetup.add_key_level_controls( 

2288 key_metadata=key_metadata, 

2289 data=data, 

2290 _metadata_variable_name=_metadata_variable_name, 

2291 ) 

2292 ## TEAM-LEVEL SPEND LOGS/TAGS 

2293 team_metadata: Final = user_api_key_dict.team_metadata or {} 

2294 if "tags" in team_metadata and team_metadata["tags"] is not None: 2294 ↛ 2295line 2294 didn't jump to line 2295 because the condition on line 2294 was never true

2295 data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( 

2296 request_tags=data[_metadata_variable_name].get("tags"), 

2297 tags_to_add=team_metadata["tags"], 

2298 ) 

2299 if "disable_global_guardrails" in team_metadata and isinstance(team_metadata["disable_global_guardrails"], bool): 2299 ↛ 2300line 2299 didn't jump to line 2300 because the condition on line 2299 was never true

2300 data[_metadata_variable_name]["disable_global_guardrails"] = team_metadata["disable_global_guardrails"] 

2301 if "opted_out_global_guardrails" in team_metadata and isinstance( 2301 ↛ 2304line 2301 didn't jump to line 2304 because the condition on line 2301 was never true

2302 team_metadata["opted_out_global_guardrails"], list 

2303 ): 

2304 data[_metadata_variable_name]["opted_out_global_guardrails"] = team_metadata["opted_out_global_guardrails"] 

2305 if "spend_logs_metadata" in team_metadata and isinstance(team_metadata["spend_logs_metadata"], dict): 2305 ↛ 2306line 2305 didn't jump to line 2306 because the condition on line 2305 was never true

2306 if "spend_logs_metadata" in data[_metadata_variable_name] and isinstance( 

2307 data[_metadata_variable_name]["spend_logs_metadata"], dict 

2308 ): 

2309 for key, value in team_metadata["spend_logs_metadata"].items(): 

2310 if ( 

2311 key not in data[_metadata_variable_name]["spend_logs_metadata"] 

2312 ): # don't override k-v pair sent by request (user request) 

2313 data[_metadata_variable_name]["spend_logs_metadata"][key] = value 

2314 else: 

2315 data[_metadata_variable_name]["spend_logs_metadata"] = team_metadata["spend_logs_metadata"] 

2316 

2317 ## PROJECT-LEVEL TAGS 

2318 project_metadata: Final = user_api_key_dict.project_metadata or {} 

2319 if "tags" in project_metadata and project_metadata["tags"] is not None: 2319 ↛ 2320line 2319 didn't jump to line 2320 because the condition on line 2319 was never true

2320 data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( 

2321 request_tags=data[_metadata_variable_name].get("tags"), 

2322 tags_to_add=project_metadata["tags"], 

2323 ) 

2324 

2325 # inherited_tags: every tag key/team/project policy contributed, read 

2326 # directly from those three sources rather than snapshotted off the shared 

2327 # "tags" list. A pre-auth pass (apply_client_tag_policy_pre_auth, run from 

2328 # user_api_key_auth for _tag_max_budget_check) may already have merged the 

2329 # caller's own header tags into that same list before this function ever 

2330 # runs, so a snapshot taken here -- at any point in this function -- would 

2331 # misattribute caller-supplied tags as policy-backed. tag_based_routing.py's 

2332 # allow_fail_open reads this (rather than subtracting caller_tags from the 

2333 # final merged set) so a caller can't strip an inherited "!"/"&" 

2334 # constraint's protection just by resubmitting its exact value alongside a 

2335 # conflicting one. 

2336 _key_tags: Final = (key_metadata or MappingProxyType({})).get("tags") or () 

2337 _team_tags: Final = team_metadata.get("tags") or () 

2338 _project_tags: Final = project_metadata.get("tags") or () 

2339 data[_metadata_variable_name]["inherited_tags"] = tuple( # rebind-ok: matches this file's data[...] mutation idiom 

2340 dict.fromkeys((*_key_tags, *_team_tags, *_project_tags)) 

2341 ) 

2342 

2343 ## TEAM-LEVEL METADATA 

2344 data = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata( 

2345 data=data, 

2346 management_endpoint_metadata=team_metadata, 

2347 _metadata_variable_name=_metadata_variable_name, 

2348 ) 

2349 

2350 # A key's OTel service name outranks its team's, so the key's values are 

2351 # re-applied after the last-writer-wins team metadata merge above 

2352 _key_otel_service_names: Final = { 

2353 field: value 

2354 for field, value in (key_metadata or {}).items() 

2355 if field in OTEL_SERVICE_NAME_METADATA_KEYS and isinstance(value, str) and value.strip() 

2356 } 

2357 data = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata( 

2358 data=data, 

2359 management_endpoint_metadata=_key_otel_service_names, 

2360 _metadata_variable_name=_metadata_variable_name, 

2361 ) 

2362 

2363 # Team spend, budget - used by prometheus.py 

2364 data[_metadata_variable_name]["user_api_key_team_max_budget"] = user_api_key_dict.team_max_budget 

2365 data[_metadata_variable_name]["user_api_key_team_spend"] = user_api_key_dict.team_spend 

2366 data[_metadata_variable_name]["user_api_key_team_model_max_budget"] = user_api_key_dict.team_model_max_budget 

2367 data[_metadata_variable_name]["user_api_key_request_route"] = user_api_key_dict.request_route 

2368 

2369 # API Key spend, budget - used by prometheus.py 

2370 data[_metadata_variable_name]["user_api_key_spend"] = user_api_key_dict.spend 

2371 data[_metadata_variable_name]["user_api_key_max_budget"] = user_api_key_dict.max_budget 

2372 data[_metadata_variable_name]["user_api_key_model_max_budget"] = user_api_key_dict.model_max_budget 

2373 data[_metadata_variable_name]["user_api_key_end_user_model_max_budget"] = ( 

2374 user_api_key_dict.end_user_model_max_budget 

2375 ) 

2376 

2377 # User spend, budget - used by prometheus.py 

2378 # Follow same pattern as team and API key budgets 

2379 data[_metadata_variable_name]["user_api_key_user_spend"] = user_api_key_dict.user_spend 

2380 data[_metadata_variable_name]["user_api_key_user_max_budget"] = user_api_key_dict.user_max_budget 

2381 user_model_budget: Final = user_api_key_dict.user_model_max_budget 

2382 data[_metadata_variable_name]["user_api_key_user_model_max_budget"] = user_model_budget # rebind-ok: out-param 

2383 data[_metadata_variable_name].update(carried_budget_metadata(user_api_key_dict)) 

2384 

2385 data[_metadata_variable_name]["user_api_key_metadata"] = strip_callback_config(user_api_key_dict.metadata) 

2386 data[_metadata_variable_name]["user_api_key_team_metadata"] = strip_callback_config(user_api_key_dict.team_metadata) 

2387 data[_metadata_variable_name]["user_api_key_object_permission_id"] = getattr( 

2388 user_api_key_dict, "object_permission_id", None 

2389 ) 

2390 data[_metadata_variable_name]["user_api_key_team_object_permission_id"] = getattr( 

2391 user_api_key_dict, "team_object_permission_id", None 

2392 ) 

2393 data[_metadata_variable_name]["headers"] = _logging_safe_headers 

2394 data[_metadata_variable_name]["endpoint"] = str(request.url) 

2395 # Carry the proxy-receive instant via metadata (like `endpoint`) so the 

2396 # OTel layer can compute pre-request latency, including on the failure 

2397 # path after the logging object is popped. 

2398 data[_metadata_variable_name]["litellm_received_at"] = getattr(request.state, "litellm_received_at", None) 

2399 data[_metadata_variable_name]["llm_api_timing_windows"] = () 

2400 

2401 # OTEL Controls / Tracing 

2402 # Add the OTEL Parent Trace before sending it LiteLLM 

2403 data[_metadata_variable_name]["litellm_parent_otel_span"] = user_api_key_dict.parent_otel_span 

2404 _add_otel_traceparent_to_data(data, request=request) 

2405 

2406 ### END-USER SPECIFIC PARAMS ### 

2407 if user_api_key_dict.allowed_model_region is not None: 2407 ↛ 2408line 2407 didn't jump to line 2408 because the condition on line 2407 was never true

2408 data["allowed_model_region"] = user_api_key_dict.allowed_model_region 

2409 start_time: Final = time.time() 

2410 ## [Enterprise Only] 

2411 # Add User-IP Address 

2412 requester_ip_address = "" 

2413 if True: # Always set the IP Address if available 

2414 # logic for tracking IP Address 

2415 

2416 # logic for tracking IP Address 

2417 if ( 2417 ↛ 2424line 2417 didn't jump to line 2424 because the condition on line 2417 was never true

2418 general_settings is not None 

2419 and general_settings.get("use_x_forwarded_for") is True 

2420 and request is not None 

2421 and hasattr(request, "headers") 

2422 and "x-forwarded-for" in request.headers 

2423 ): 

2424 requester_ip_address = request.headers["x-forwarded-for"] 

2425 elif ( 2425 ↛ 2432line 2425 didn't jump to line 2432 because the condition on line 2425 was always true

2426 request is not None 

2427 and hasattr(request, "client") 

2428 and hasattr(request.client, "host") 

2429 and request.client is not None 

2430 ): 

2431 requester_ip_address = request.client.host 

2432 data[_metadata_variable_name]["requester_ip_address"] = requester_ip_address 

2433 

2434 # Add User-Agent 

2435 user_agent = "" 

2436 if request is not None and hasattr(request, "headers") and "user-agent" in request.headers: 

2437 user_agent = request.headers["user-agent"] 

2438 data[_metadata_variable_name]["user_agent"] = user_agent 

2439 

2440 if should_auto_drop_params_for_agentic_cli(user_agent, data, proxy_config): 2440 ↛ 2441line 2440 didn't jump to line 2441 because the condition on line 2440 was never true

2441 data["drop_params"] = True 

2442 

2443 # Merge caller-supplied tags (x-litellm-tags header, data["tags"] root-level) 

2444 # into request metadata for tag-based routing and spend attribution. 

2445 tags: Final = LiteLLMProxyRequestSetup.add_request_tag_to_metadata( 

2446 llm_router=llm_router, 

2447 headers=_headers, 

2448 data=data, 

2449 ) 

2450 

2451 if tags is not None: 2451 ↛ 2452line 2451 didn't jump to line 2452 because the condition on line 2451 was never true

2452 data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( 

2453 request_tags=data[_metadata_variable_name].get("tags"), 

2454 tags_to_add=tags, 

2455 ) 

2456 

2457 _caller_body_metadata: Final = data.get("metadata") if _metadata_variable_name != "metadata" else None 

2458 _caller_body_tags: Final = ( 

2459 _caller_body_metadata.get("tags") 

2460 if isinstance(_caller_body_metadata, dict) and isinstance(_caller_body_metadata.get("tags"), list) 

2461 else None 

2462 ) 

2463 if _caller_body_tags: 2463 ↛ 2464line 2463 didn't jump to line 2464 because the condition on line 2463 was never true

2464 data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( # rebind-ok: matches file idiom 

2465 request_tags=data[_metadata_variable_name].get("tags"), 

2466 tags_to_add=_caller_body_tags, 

2467 ) 

2468 

2469 # caller_tags: exactly what this request itself supplied (x-litellm-tags header, 

2470 # body "tags", or body "metadata.tags" on litellm_metadata routes), never 

2471 # anything from key/team/project metadata. Read directly from the header and 

2472 # body values here, the same way inherited_tags above is read directly from 

2473 # key/team/project metadata -- neither is derived by inspecting the shared 

2474 # "tags" list, which a pre-auth pass (apply_client_tag_policy_pre_auth) may 

2475 # have already merged caller header tags into before this function runs. 

2476 data[_metadata_variable_name]["caller_tags"] = tuple( # rebind-ok: matches file idiom 

2477 dict.fromkeys((*(tags or ()), *(_caller_body_tags or ()))) 

2478 ) 

2479 

2480 # Team Callbacks controls 

2481 callback_settings_obj: Final = _get_dynamic_logging_metadata( 

2482 user_api_key_dict=user_api_key_dict, proxy_config=proxy_config 

2483 ) 

2484 if callback_settings_obj is not None: 2484 ↛ 2485line 2484 didn't jump to line 2485 because the condition on line 2484 was never true

2485 data["success_callback"] = callback_settings_obj.success_callback 

2486 data["failure_callback"] = callback_settings_obj.failure_callback 

2487 

2488 if callback_settings_obj.callback_vars is not None: 

2489 # unpack callback_vars in data 

2490 for k, v in callback_settings_obj.callback_vars.items(): 

2491 data[k] = v 

2492 # Callbacks that must not honour request-supplied credentials read this 

2493 # proxy-owned field instead of the raw request kwargs. 

2494 data[TRUSTED_CALLBACK_VARS_FIELD] = callback_settings_obj.callback_vars 

2495 

2496 # Add disabled callbacks from key metadata 

2497 if user_api_key_dict.metadata and "litellm_disabled_callbacks" in user_api_key_dict.metadata: 2497 ↛ 2498line 2497 didn't jump to line 2498 because the condition on line 2497 was never true

2498 disabled_callbacks: Final = user_api_key_dict.metadata["litellm_disabled_callbacks"] 

2499 if disabled_callbacks and isinstance(disabled_callbacks, list): 

2500 data["litellm_disabled_callbacks"] = disabled_callbacks 

2501 

2502 # Guardrails from key/team metadata and policy engine 

2503 await move_guardrails_to_metadata( 

2504 data=data, 

2505 _metadata_variable_name=_metadata_variable_name, 

2506 user_api_key_dict=user_api_key_dict, 

2507 ) 

2508 

2509 # Save pre-alias model name for credential override lookup 

2510 _pre_alias_model: Final = data.get("model") 

2511 

2512 # Team Model Aliases 

2513 _update_model_if_team_alias_exists( 

2514 data=data, 

2515 user_api_key_dict=user_api_key_dict, 

2516 ) 

2517 

2518 # Key Model Aliases 

2519 _update_model_if_key_alias_exists( 

2520 data=data, 

2521 user_api_key_dict=user_api_key_dict, 

2522 ) 

2523 

2524 verbose_proxy_logger.debug("[PROXY] returned data from litellm_pre_call_utils: %s", data) 

2525 

2526 # Team/Project credential overrides from model_config 

2527 # Placed after the debug log to avoid leaking credential secrets in logs 

2528 _apply_credential_overrides_from_model_config( 

2529 data=data, 

2530 user_api_key_dict=user_api_key_dict, 

2531 pre_alias_model_name=_pre_alias_model, 

2532 llm_router=llm_router, 

2533 ) 

2534 

2535 ## ENFORCED PARAMS CHECK 

2536 # loop through each enforced param 

2537 # example enforced_params ['user', 'metadata', 'metadata.generation_name'] 

2538 _enforced_params_check( 

2539 request_body=data, 

2540 general_settings=general_settings, 

2541 user_api_key_dict=user_api_key_dict, 

2542 premium_user=premium_user, 

2543 ) 

2544 

2545 end_time: Final = time.time() 

2546 asyncio.create_task( 

2547 service_logger_obj.async_service_success_hook( 

2548 service=ServiceTypes.PROXY_PRE_CALL, 

2549 duration=end_time - start_time, 

2550 call_type="add_litellm_data_to_request", 

2551 start_time=start_time, 

2552 end_time=end_time, 

2553 parent_otel_span=user_api_key_dict.parent_otel_span, 

2554 ) 

2555 ) 

2556 

2557 return data 

2558 

2559 

2560def _warn_stale_team_alias_once(warning_key: str, message: str, *args: str) -> None: 

2561 if warning_key in _STALE_TEAM_ALIAS_WARNING_KEYS: 

2562 return 

2563 _STALE_TEAM_ALIAS_WARNING_KEYS[warning_key] = None 

2564 while len(_STALE_TEAM_ALIAS_WARNING_KEYS) > _MAX_STALE_ALIAS_WARNING_KEYS: 

2565 _STALE_TEAM_ALIAS_WARNING_KEYS.popitem(last=False) 

2566 verbose_proxy_logger.warning(message, *args) 

2567 

2568 

2569def _update_model_if_team_alias_exists( 

2570 data: dict, 

2571 user_api_key_dict: UserAPIKeyAuth, 

2572) -> None: 

2573 """ 

2574 Update the model if the team alias exists 

2575 

2576 If a alias map has been set on a team, then we want to make the request with the model the team alias is pointing to 

2577 

2578 eg. 

2579 - user calls `gpt-4o` 

2580 - team.model_alias_map = { 

2581 "gpt-4o": "gpt-4o-team-1" 

2582 } 

2583 - requested_model = "gpt-4o-team-1" 

2584 

2585 Note: model_aliases for team models are deprecated. This function only applies 

2586 to legacy non-team-scoped aliases. Team-scoped deployments use team_public_model_name 

2587 and are resolved via map_team_model in route_llm_request. 

2588 

2589 An alias that targets a team-scoped internal name (``model_name_{team_id}_{uuid}``) 

2590 with no live deployment behind it is never applied: the deployment was deleted, so 

2591 the rewrite could only fail with an error naming a model the caller never sent. 

2592 Keeping the requested model name lets it resolve against the deployments that still 

2593 exist (e.g. a gateway-level model group shared with the team). 

2594 """ 

2595 _model: Final = data.get("model") 

2596 if not _model or not user_api_key_dict.team_model_aliases or _model not in user_api_key_dict.team_model_aliases: 2596 ↛ 2599line 2596 didn't jump to line 2599 because the condition on line 2596 was always true

2597 return 

2598 

2599 from litellm.proxy.proxy_server import llm_router 

2600 

2601 # Skip alias rewrite if this model resolves to team-specific deployments 

2602 # (team models use team_public_model_name, not model_aliases) 

2603 aliased_target: Final = user_api_key_dict.team_model_aliases[_model] 

2604 

2605 # Optional bypass for stale aliases from pre-PR deployments: 

2606 # only enabled via feature flag to preserve backwards compatibility. 

2607 # Cached at module level to avoid hot-path secret lookups on every request. 

2608 global _ENABLE_TEAM_STALE_ALIAS_BYPASS 

2609 if _ENABLE_TEAM_STALE_ALIAS_BYPASS is None: 

2610 _ENABLE_TEAM_STALE_ALIAS_BYPASS = get_secret_bool("LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS", False) 

2611 enable_stale_alias_bypass: Final = _ENABLE_TEAM_STALE_ALIAS_BYPASS 

2612 # Check if the alias points to a team-scoped UUID name 

2613 # (format: "model_name_{team_id}_{uuid}") 

2614 is_stale_team_alias: Final = aliased_target.startswith(f"model_name_{user_api_key_dict.team_id}_") 

2615 if is_stale_team_alias and llm_router: 

2616 if aliased_target not in llm_router.model_name_to_deployment_indices: 

2617 _warn_stale_team_alias_once( 

2618 f"deleted:{user_api_key_dict.team_id}:{_model}:{aliased_target}", 

2619 "Team model alias for model='%s', team_id='%s' targets '%s', which has no live " 

2620 "deployment. Routing with the requested model name instead; remove the stale " 

2621 "entry from the team's model_aliases to silence this warning.", 

2622 _sanitize_for_log(_model), 

2623 _sanitize_for_log(user_api_key_dict.team_id), 

2624 _sanitize_for_log(aliased_target), 

2625 ) 

2626 return 

2627 # This is a stale alias from pre-PR deployments. 

2628 # Check if current team deployments exist for the public name. 

2629 key: Final = (user_api_key_dict.team_id, _model) 

2630 if key in llm_router.team_model_to_deployment_indices: 

2631 if enable_stale_alias_bypass: 

2632 # Team deployments exist; skip stale alias 

2633 return 

2634 _warn_stale_team_alias_once( 

2635 f"{user_api_key_dict.team_id}:{_model}:{aliased_target}", 

2636 "Stale team model alias detected for model='%s', team_id='%s'. " 

2637 "New sibling deployments may be unreachable. " 

2638 "Set LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS=true to enable " 

2639 "team-scoped sibling routing.", 

2640 _sanitize_for_log(_model), 

2641 _sanitize_for_log(user_api_key_dict.team_id), 

2642 ) 

2643 

2644 data["model"] = aliased_target 

2645 

2646 

2647def _update_model_if_key_alias_exists( 

2648 data: dict, 

2649 user_api_key_dict: UserAPIKeyAuth, 

2650) -> None: 

2651 """ 

2652 Update the model if the key alias exists 

2653 

2654 If an alias map has been set on a key, then we want to make the request with the model the key alias is pointing to 

2655 

2656 eg. 

2657 - user calls `modelAlias` 

2658 - key.aliases = { 

2659 "modelAlias": "xai/grok-4-fast-non-reasoning" 

2660 } 

2661 - requested_model = "xai/grok-4-fast-non-reasoning" 

2662 """ 

2663 _model: Final = data.get("model") 

2664 if ( 2664 ↛ 2670line 2664 didn't jump to line 2670 because the condition on line 2664 was never true

2665 _model 

2666 and user_api_key_dict.aliases 

2667 and isinstance(user_api_key_dict.aliases, dict) 

2668 and _model in user_api_key_dict.aliases 

2669 ): 

2670 data["model"] = user_api_key_dict.aliases[_model] 

2671 

2672 

2673def _apply_credential_overrides_from_model_config( 

2674 data: dict, 

2675 user_api_key_dict: UserAPIKeyAuth, 

2676 pre_alias_model_name: str | None = None, 

2677 llm_router: Router | None = None, 

2678) -> None: 

2679 """ 

2680 Walk the model_config precedence chain in team/project metadata. 

2681 If a matching credential is found, set api_base/api_key/api_version on data 

2682 so they override deployment defaults in the router. 

2683 

2684 Precedence (highest to lowest): 

2685 1. Clientside credentials (already in data — skip if present) 

2686 2. Project model-specific override 

2687 3. Project default override (defaultconfig) 

2688 4. Team model-specific override 

2689 5. Team default override (defaultconfig) 

2690 6. Deployment default (no action needed) 

2691 """ 

2692 # Feature flag gate — disabled by default, opt in with litellm.enable_model_config_credential_overrides = True 

2693 if not litellm.enable_model_config_credential_overrides: 2693 ↛ 2697line 2693 didn't jump to line 2697 because the condition on line 2693 was always true

2694 return 

2695 

2696 # Respect clientside credentials — highest precedence 

2697 if data.get("api_base") is not None or data.get("api_key") is not None: 

2698 return 

2699 

2700 model_name: Final = data.get("model") 

2701 if not model_name: 

2702 return 

2703 

2704 project_metadata: Final = user_api_key_dict.project_metadata or {} 

2705 team_metadata: Final = user_api_key_dict.team_metadata or {} 

2706 

2707 project_model_config: Final = project_metadata.get("model_config") 

2708 team_model_config: Final = team_metadata.get("model_config") 

2709 

2710 if not project_model_config and not team_model_config: 

2711 return 

2712 

2713 # Extract provider hint from model name (e.g. "azure/gpt-4" -> "azure"). 

2714 # When the user-facing name has no provider prefix, fall back to the 

2715 # deployment's litellm_params so multi-provider defaultconfig entries 

2716 # don't silently match the first dict key (#27516). 

2717 provider: str | None = None 

2718 if "/" in model_name: 

2719 provider = model_name.split("/", 1)[0] 

2720 elif llm_router is not None: 

2721 provider = _resolve_provider_from_deployment( 

2722 llm_router=llm_router, 

2723 model_name=model_name, 

2724 pre_alias_model_name=pre_alias_model_name, 

2725 ) 

2726 

2727 credential_name: Final = _resolve_credential_from_model_config( 

2728 model_name=model_name, 

2729 project_model_config=project_model_config, 

2730 team_model_config=team_model_config, 

2731 pre_alias_model_name=pre_alias_model_name, 

2732 provider=provider, 

2733 ) 

2734 

2735 if not credential_name: 

2736 return 

2737 

2738 credential_values: Final = CredentialAccessor.get_credential_values(credential_name) 

2739 if not credential_values: 

2740 _safe_cred = str(credential_name).replace("\n", "").replace("\r", "") 

2741 verbose_proxy_logger.warning( 

2742 "model_config references credential '%s' but it was not found or has no values", 

2743 _safe_cred, 

2744 ) 

2745 return 

2746 

2747 # Apply credential overrides only for keys not already in the request 

2748 for key in ("api_base", "api_key", "api_version"): 

2749 if key in credential_values and key not in data: 

2750 data[key] = credential_values[key] 

2751 

2752 _safe_model: Final = str(model_name).replace("\n", "").replace("\r", "") 

2753 _safe_cred = str(credential_name).replace("\n", "").replace("\r", "") 

2754 verbose_proxy_logger.debug( 

2755 "Applied credential override '%s' for model '%s'", 

2756 _safe_cred, 

2757 _safe_model, 

2758 ) 

2759 

2760 

2761def _resolve_provider_from_deployment( 

2762 llm_router: Router, 

2763 model_name: str, 

2764 pre_alias_model_name: str | None = None, 

2765) -> str | None: 

2766 """ 

2767 Resolve a provider hint from the deployment's litellm_params when the 

2768 user-facing model name has no provider prefix. 

2769 

2770 Tries the post-alias name first (the resolved model group), then the 

2771 pre-alias name. Returns None if no deployment is found or the deployment 

2772 has no usable provider info. 

2773 """ 

2774 candidates: Final = [model_name] 

2775 if pre_alias_model_name and pre_alias_model_name != model_name: 

2776 candidates.append(pre_alias_model_name) 

2777 

2778 for name in candidates: 

2779 try: 

2780 deployment = llm_router.get_deployment_by_model_group_name(model_group_name=name) 

2781 except Exception: 

2782 deployment = None 

2783 if deployment is None: 

2784 continue 

2785 

2786 litellm_params: object = getattr(deployment, "litellm_params", None) 

2787 if litellm_params is None: 

2788 continue 

2789 

2790 custom_provider = getattr(litellm_params, "custom_llm_provider", None) 

2791 if isinstance(custom_provider, str) and custom_provider: 

2792 return custom_provider 

2793 

2794 deployment_model = getattr(litellm_params, "model", "") 

2795 if isinstance(deployment_model, str) and "/" in deployment_model: 

2796 return deployment_model.split("/", 1)[0] 

2797 

2798 return None 

2799 

2800 

2801def _resolve_credential_from_model_config( 

2802 model_name: str, 

2803 project_model_config: dict | None, 

2804 team_model_config: dict | None, 

2805 pre_alias_model_name: str | None = None, 

2806 provider: str | None = None, 

2807) -> str | None: 

2808 """ 

2809 Walk the precedence chain and return the first matching credential name. 

2810 

2811 Checks (in order): 

2812 1. project_model_config[model_name][provider] — project model-specific 

2813 2. project_model_config[pre_alias_model_name][provider] — project pre-alias 

2814 3. project_model_config["defaultconfig"][provider] — project default 

2815 4. team_model_config[model_name][provider] — team model-specific 

2816 5. team_model_config[pre_alias_model_name][provider] — team pre-alias 

2817 6. team_model_config["defaultconfig"][provider] — team default 

2818 

2819 When a model-specific entry exists but contains no litellm_credentials, 

2820 the function falls through to defaultconfig. This is intentional — 

2821 an entry without litellm_credentials is treated as incomplete config, 

2822 not as an explicit "no override" signal. 

2823 """ 

2824 # Build the list of model names to try (post-alias first, then pre-alias) 

2825 model_names_to_try: Final = [model_name] 

2826 if pre_alias_model_name and pre_alias_model_name != model_name: 

2827 model_names_to_try.append(pre_alias_model_name) 

2828 

2829 for model_config in (project_model_config, team_model_config): 

2830 if not model_config or not isinstance(model_config, dict): 

2831 continue 

2832 

2833 # Model-specific check (try resolved name, then pre-alias name) 

2834 for name in model_names_to_try: 

2835 model_entry = model_config.get(name) 

2836 if model_entry: 

2837 credential_name = _extract_credential_from_entry(model_entry, provider=provider) 

2838 if credential_name: 

2839 return credential_name 

2840 _safe_name = str(name).replace("\n", "").replace("\r", "") 

2841 verbose_proxy_logger.debug( 

2842 "model_config entry '%s' found but has no litellm_credentials, trying next candidate", 

2843 _safe_name, 

2844 ) 

2845 

2846 # Default check 

2847 default_entry = model_config.get("defaultconfig") 

2848 if default_entry: 

2849 credential_name = _extract_credential_from_entry(default_entry, provider=provider) 

2850 if credential_name: 

2851 return credential_name 

2852 

2853 return None 

2854 

2855 

2856def _extract_credential_from_entry(entry: dict, provider: str | None = None) -> str | None: 

2857 """ 

2858 Extract litellm_credentials from a model_config entry. 

2859 

2860 Entry structure: {"azure": {"litellm_credentials": "name"}, ...} 

2861 

2862 When provider is given (e.g. "azure"), tries an exact provider match first. 

2863 Falls back to the first credential found across all provider keys. 

2864 """ 

2865 if not isinstance(entry, dict): 

2866 return None 

2867 

2868 # Prefer exact provider match when provider hint is available 

2869 if provider and provider in entry: 

2870 provider_config = entry[provider] 

2871 if isinstance(provider_config, dict): 

2872 credential_name = provider_config.get("litellm_credentials") 

2873 if credential_name: 

2874 return credential_name 

2875 

2876 # Fall back to first available provider 

2877 for provider_config in entry.values(): 

2878 if isinstance(provider_config, dict): 

2879 credential_name = provider_config.get("litellm_credentials") 

2880 if credential_name: 

2881 return credential_name 

2882 return None 

2883 

2884 

2885def _get_enforced_params(general_settings: dict | None, user_api_key_dict: UserAPIKeyAuth) -> list | None: 

2886 enforced_params: list | None = None 

2887 if general_settings is not None: 2887 ↛ 2898line 2887 didn't jump to line 2898 because the condition on line 2887 was always true

2888 enforced_params = general_settings.get("enforced_params") 

2889 if ( 2889 ↛ 2893line 2889 didn't jump to line 2893 because the condition on line 2889 was never true

2890 "service_account_settings" in general_settings 

2891 and check_if_token_is_service_account(user_api_key_dict) is True 

2892 ): 

2893 service_account_settings: Final = general_settings["service_account_settings"] 

2894 if "enforced_params" in service_account_settings: 

2895 if enforced_params is None: 

2896 enforced_params = [] 

2897 enforced_params.extend(service_account_settings["enforced_params"]) 

2898 if user_api_key_dict.metadata.get("enforced_params", None) is not None: 2898 ↛ 2899line 2898 didn't jump to line 2899 because the condition on line 2898 was never true

2899 if enforced_params is None: 

2900 enforced_params = [] 

2901 enforced_params.extend(user_api_key_dict.metadata["enforced_params"]) 

2902 return enforced_params 

2903 

2904 

2905def check_if_token_is_service_account(valid_token: UserAPIKeyAuth) -> bool: 

2906 """ 

2907 Checks if the token is a service account 

2908 

2909 Returns: 

2910 bool: True if token is a service account 

2911 

2912 """ 

2913 if valid_token.metadata: 

2914 if "service_account_id" in valid_token.metadata: 

2915 return True 

2916 return False 

2917 

2918 

2919def _enforced_params_check( 

2920 request_body: dict, 

2921 general_settings: dict | None, 

2922 user_api_key_dict: UserAPIKeyAuth, 

2923 premium_user: bool, 

2924) -> bool: 

2925 """ 

2926 If enforced params are set, check if the request body contains the enforced params. 

2927 """ 

2928 enforced_params: Final[list | None] = _get_enforced_params( 

2929 general_settings=general_settings, user_api_key_dict=user_api_key_dict 

2930 ) 

2931 if enforced_params is None: 2931 ↛ 2933line 2931 didn't jump to line 2933 because the condition on line 2931 was always true

2932 return True 

2933 if enforced_params and premium_user is not True: 

2934 raise ValueError( 

2935 f"Enforced Params is an Enterprise feature. Enforced Params: {enforced_params}. {CommonProxyErrors.not_premium_user.value}" 

2936 ) 

2937 

2938 for enforced_param in enforced_params: 

2939 _enforced_params = enforced_param.split(".") 

2940 if len(_enforced_params) == 1: 

2941 if _enforced_params[0] not in request_body: 

2942 raise ValueError( 

2943 f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param" 

2944 ) 

2945 elif len(_enforced_params) == 2: 

2946 # this is a scenario where user requires request['metadata']['generation_name'] to exist 

2947 if _enforced_params[0] not in request_body: 

2948 raise ValueError( 

2949 f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param" 

2950 ) 

2951 if _enforced_params[1] not in request_body[_enforced_params[0]]: 

2952 raise ValueError( 

2953 f"BadRequest please pass param=[{_enforced_params[0]}][{_enforced_params[1]}] in request body. This is a required param" 

2954 ) 

2955 return True 

2956 

2957 

2958def _add_guardrails_from_key_or_team_metadata( 

2959 key_metadata: dict | None, 

2960 team_metadata: dict | None, 

2961 data: dict, 

2962 metadata_variable_name: str, 

2963 project_metadata: dict | None = None, 

2964) -> None: 

2965 """ 

2966 Helper add guardrails from key, team, or project metadata to request data 

2967 

2968 Key guardrails are set first, then team and project guardrails are appended (without duplicates). 

2969 

2970 Args: 

2971 key_metadata: The key metadata dictionary to check for guardrails 

2972 team_metadata: The team metadata dictionary to check for guardrails 

2973 data: The request data to update 

2974 metadata_variable_name: The name of the metadata field in data 

2975 project_metadata: The project metadata dictionary to check for guardrails 

2976 

2977 """ 

2978 from litellm.proxy.utils import _premium_user_check 

2979 

2980 # Initialize guardrails set (avoiding duplicates) 

2981 combined_guardrails: Final = set() 

2982 

2983 # Add key-level guardrails first 

2984 if key_metadata and "guardrails" in key_metadata: 2984 ↛ 2985line 2984 didn't jump to line 2985 because the condition on line 2984 was never true

2985 if isinstance(key_metadata["guardrails"], list) and len(key_metadata["guardrails"]) > 0: 

2986 _premium_user_check() 

2987 combined_guardrails.update(key_metadata["guardrails"]) 

2988 

2989 # Add team-level guardrails (set automatically handles duplicates) 

2990 if team_metadata and "guardrails" in team_metadata: 2990 ↛ 2991line 2990 didn't jump to line 2991 because the condition on line 2990 was never true

2991 if isinstance(team_metadata["guardrails"], list) and len(team_metadata["guardrails"]) > 0: 

2992 _premium_user_check() 

2993 combined_guardrails.update(team_metadata["guardrails"]) 

2994 

2995 # Add project-level guardrails (set automatically handles duplicates) 

2996 if project_metadata and "guardrails" in project_metadata: 2996 ↛ 2997line 2996 didn't jump to line 2997 because the condition on line 2996 was never true

2997 if isinstance(project_metadata["guardrails"], list) and len(project_metadata["guardrails"]) > 0: 

2998 _premium_user_check() 

2999 combined_guardrails.update(project_metadata["guardrails"]) 

3000 

3001 # Set combined guardrails in metadata as list 

3002 if combined_guardrails: 3002 ↛ 3003line 3002 didn't jump to line 3003 because the condition on line 3002 was never true

3003 data[metadata_variable_name]["guardrails"] = list(combined_guardrails) 

3004 

3005 

3006def _add_guardrails_from_policies_in_metadata( 

3007 key_metadata: dict | None, 

3008 team_metadata: dict | None, 

3009 data: dict, 

3010 metadata_variable_name: str, 

3011 project_metadata: dict | None = None, 

3012) -> None: 

3013 """ 

3014 Helper to resolve guardrails from policies attached to key/team/project metadata. 

3015 

3016 This function: 

3017 1. Gets policy names from key, team, and project metadata 

3018 2. Resolves guardrails from those policies (including inheritance) 

3019 3. Adds resolved guardrails to request metadata 

3020 

3021 Args: 

3022 key_metadata: The key metadata dictionary to check for policies 

3023 team_metadata: The team metadata dictionary to check for policies 

3024 data: The request data to update 

3025 metadata_variable_name: The name of the metadata field in data 

3026 project_metadata: The project metadata dictionary to check for policies 

3027 """ 

3028 from litellm._logging import verbose_proxy_logger 

3029 from litellm.proxy.policy_engine.policy_registry import get_policy_registry 

3030 from litellm.proxy.policy_engine.policy_resolver import PolicyResolver 

3031 from litellm.proxy.utils import _premium_user_check 

3032 from litellm.types.proxy.policy_engine import PolicyMatchContext 

3033 

3034 # Collect policy names from key and team metadata 

3035 policy_names: Final[set] = set() 

3036 

3037 # Add key-level policies first 

3038 if key_metadata and "policies" in key_metadata: 3038 ↛ 3039line 3038 didn't jump to line 3039 because the condition on line 3038 was never true

3039 if isinstance(key_metadata["policies"], list) and len(key_metadata["policies"]) > 0: 

3040 _premium_user_check() 

3041 policy_names.update(key_metadata["policies"]) 

3042 

3043 # Add team-level policies 

3044 if team_metadata and "policies" in team_metadata: 3044 ↛ 3045line 3044 didn't jump to line 3045 because the condition on line 3044 was never true

3045 if isinstance(team_metadata["policies"], list) and len(team_metadata["policies"]) > 0: 

3046 _premium_user_check() 

3047 policy_names.update(team_metadata["policies"]) 

3048 

3049 # Add project-level policies 

3050 if project_metadata and "policies" in project_metadata: 3050 ↛ 3051line 3050 didn't jump to line 3051 because the condition on line 3050 was never true

3051 if isinstance(project_metadata["policies"], list) and len(project_metadata["policies"]) > 0: 

3052 _premium_user_check() 

3053 policy_names.update(project_metadata["policies"]) 

3054 

3055 if not policy_names: 3055 ↛ 3058line 3055 didn't jump to line 3058 because the condition on line 3055 was always true

3056 return 

3057 

3058 verbose_proxy_logger.debug("Policy engine: resolving guardrails from key/team policies: %s", policy_names) 

3059 

3060 # Check if policy registry is initialized 

3061 registry: Final = get_policy_registry() 

3062 if not registry.is_initialized(): 

3063 verbose_proxy_logger.debug("Policy engine not initialized, skipping policy resolution from metadata") 

3064 return 

3065 

3066 # Build context for policy resolution (model from request data) 

3067 context: Final = PolicyMatchContext(model=data.get("model")) 

3068 

3069 # Get all policies from registry 

3070 all_policies: Final = registry.get_all_policies() 

3071 

3072 # Resolve guardrails from the specified policies 

3073 resolved_guardrails: Final[set] = set() 

3074 for policy_name in policy_names: 

3075 if registry.has_policy(policy_name): 

3076 resolved_policy = PolicyResolver.resolve_policy_guardrails( 

3077 policy_name=policy_name, 

3078 policies=all_policies, 

3079 context=context, 

3080 ) 

3081 resolved_guardrails.update(resolved_policy.guardrails) 

3082 verbose_proxy_logger.debug( 

3083 "Policy engine: resolved guardrails from policy '%s': %s", policy_name, resolved_policy.guardrails 

3084 ) 

3085 else: 

3086 verbose_proxy_logger.warning("Policy engine: policy '%s' not found in registry", policy_name) 

3087 

3088 if not resolved_guardrails: 

3089 return 

3090 

3091 # Add resolved guardrails to request metadata 

3092 if metadata_variable_name not in data: 

3093 data[metadata_variable_name] = {} 

3094 

3095 existing_guardrails = data[metadata_variable_name].get("guardrails", []) 

3096 if not isinstance(existing_guardrails, list): 

3097 existing_guardrails = [] 

3098 

3099 # Combine existing guardrails with policy-resolved guardrails (no duplicates) 

3100 combined: Final = set(existing_guardrails) 

3101 combined.update(resolved_guardrails) 

3102 data[metadata_variable_name]["guardrails"] = list(combined) 

3103 

3104 # Store applied policies in metadata for tracking 

3105 if "applied_policies" not in data[metadata_variable_name]: 

3106 data[metadata_variable_name]["applied_policies"] = [] 

3107 data[metadata_variable_name]["applied_policies"].extend(list(policy_names)) 

3108 

3109 verbose_proxy_logger.debug( 

3110 "Policy engine: added guardrails from key/team policies to request metadata: %s", list(resolved_guardrails) 

3111 ) 

3112 

3113 

3114def add_guardrails_from_auth_metadata( 

3115 user_api_key_dict: UserAPIKeyAuth, 

3116 data: dict, # mutable-ok: writes guardrails into the live request dict, same contract as the helpers it wraps 

3117 metadata_variable_name: str, 

3118) -> None: 

3119 """Resolve key, team, and project guardrails, direct and via policies, onto the request metadata.""" 

3120 _add_guardrails_from_key_or_team_metadata( 

3121 key_metadata=user_api_key_dict.metadata, 

3122 team_metadata=user_api_key_dict.team_metadata, 

3123 project_metadata=user_api_key_dict.project_metadata, 

3124 data=data, 

3125 metadata_variable_name=metadata_variable_name, 

3126 ) 

3127 _add_guardrails_from_policies_in_metadata( 

3128 key_metadata=user_api_key_dict.metadata, 

3129 team_metadata=user_api_key_dict.team_metadata, 

3130 project_metadata=user_api_key_dict.project_metadata, 

3131 data=data, 

3132 metadata_variable_name=metadata_variable_name, 

3133 ) 

3134 

3135 

3136async def move_guardrails_to_metadata( 

3137 data: dict, 

3138 _metadata_variable_name: str, 

3139 user_api_key_dict: UserAPIKeyAuth, 

3140): 

3141 """ 

3142 Helper to add guardrails from request to metadata 

3143 

3144 - If guardrails set on API Key metadata then sets guardrails on request metadata 

3145 - If guardrails not set on API key, then checks request metadata 

3146 - Adds guardrails from policies attached to key/team metadata 

3147 - Adds guardrails from policy engine based on team/key/model context 

3148 - Moves include_guardrail_response into request metadata before provider dispatch 

3149 """ 

3150 if "include_guardrail_response" in data: 3150 ↛ 3151line 3150 didn't jump to line 3151 because the condition on line 3150 was never true

3151 data[_metadata_variable_name]["include_guardrail_response"] = data.pop("include_guardrail_response") is True 

3152 

3153 # Early-out: skip all guardrails processing when nothing is configured 

3154 key_metadata: Final = user_api_key_dict.metadata 

3155 team_metadata: Final = user_api_key_dict.team_metadata 

3156 project_metadata: Final = user_api_key_dict.project_metadata or {} 

3157 

3158 has_key_config: Final = key_metadata and ("guardrails" in key_metadata or "policies" in key_metadata) 

3159 has_team_config: Final = team_metadata and ("guardrails" in team_metadata or "policies" in team_metadata) 

3160 has_project_config = project_metadata and ("guardrails" in project_metadata or "policies" in project_metadata) 

3161 has_request_config: Final = "guardrails" in data or "guardrail_config" in data or "policies" in data 

3162 

3163 # Only check policy engine if no local config (avoid import + registry lookup) 

3164 if not (has_key_config or has_team_config or has_project_config or has_request_config): 

3165 from litellm.proxy.policy_engine.policy_registry import get_policy_registry 

3166 

3167 if not get_policy_registry().is_initialized(): 3167 ↛ 3169line 3167 didn't jump to line 3169 because the condition on line 3167 was never true

3168 # Nothing configured anywhere - clean up request body fields and return 

3169 data.pop("policies", None) 

3170 return 

3171 

3172 add_guardrails_from_auth_metadata( 

3173 user_api_key_dict=user_api_key_dict, 

3174 data=data, 

3175 metadata_variable_name=_metadata_variable_name, 

3176 ) 

3177 

3178 ######################################################################################### 

3179 # Add guardrails from policy engine based on team/key/model context 

3180 ######################################################################################### 

3181 await add_guardrails_from_policy_engine( 

3182 data=data, 

3183 metadata_variable_name=_metadata_variable_name, 

3184 user_api_key_dict=user_api_key_dict, 

3185 ) 

3186 

3187 ######################################################################################### 

3188 # User's might send "guardrails" in the request body, we need to add them to the request metadata. 

3189 # Since downstream logic requires "guardrails" to be in the request metadata 

3190 ######################################################################################### 

3191 if "guardrails" in data: 

3192 request_body_guardrails: Final = data.pop("guardrails") 

3193 if "guardrails" in data[_metadata_variable_name] and isinstance( 3193 ↛ 3196line 3193 didn't jump to line 3196 because the condition on line 3193 was never true

3194 data[_metadata_variable_name]["guardrails"], list 

3195 ): 

3196 data[_metadata_variable_name]["guardrails"].extend(request_body_guardrails) 

3197 else: 

3198 data[_metadata_variable_name]["guardrails"] = request_body_guardrails 

3199 

3200 ######################################################################################### 

3201 if "guardrail_config" in data: 3201 ↛ 3202line 3201 didn't jump to line 3202 because the condition on line 3201 was never true

3202 request_body_guardrail_config: Final = data.pop("guardrail_config") 

3203 if "guardrail_config" in data[_metadata_variable_name] and isinstance( 

3204 data[_metadata_variable_name]["guardrail_config"], dict 

3205 ): 

3206 data[_metadata_variable_name]["guardrail_config"].update(request_body_guardrail_config) 

3207 else: 

3208 data[_metadata_variable_name]["guardrail_config"] = request_body_guardrail_config 

3209 

3210 

3211def _is_policy_version_id(s: str) -> bool: 

3212 """Return True if string is a policy version ID (starts with policy_<uuid> prefix).""" 

3213 from litellm.proxy.policy_engine.policy_registry import POLICY_VERSION_ID_PREFIX 

3214 

3215 return isinstance(s, str) and s.startswith(POLICY_VERSION_ID_PREFIX) 

3216 

3217 

3218def _extract_policy_id(s: str) -> str | None: 

3219 """Extract raw UUID from policy_<uuid> string, or None if not a valid version ID.""" 

3220 from litellm.proxy.policy_engine.policy_registry import POLICY_VERSION_ID_PREFIX 

3221 

3222 if not _is_policy_version_id(s): 

3223 return None 

3224 return s[len(POLICY_VERSION_ID_PREFIX) :].strip() or None 

3225 

3226 

3227def _match_and_track_policies( 

3228 data: dict, 

3229 context: "PolicyMatchContext", 

3230 request_body_policies: Sequence[str], 

3231 policies_override: dict[str, "Policy"] | None = None, 

3232 attachment_registry_override: "AttachmentRegistry | None" = None, 

3233) -> tuple[list[str], dict[str, str]]: 

3234 """ 

3235 Match policies via attachments and request body, track them in metadata. 

3236 

3237 Returns: 

3238 Tuple of (applied_policy_names, policy_reasons) 

3239 """ 

3240 from litellm._logging import verbose_proxy_logger 

3241 from litellm.proxy.common_utils.callback_utils import ( 

3242 add_policy_sources_to_metadata, 

3243 add_policy_to_applied_policies_header, 

3244 ) 

3245 from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry 

3246 from litellm.proxy.policy_engine.policy_matcher import PolicyMatcher 

3247 

3248 # Get matching policies via attachments (with match reasons for attribution) 

3249 attachment_registry: Final = ( 

3250 attachment_registry_override if attachment_registry_override is not None else get_attachment_registry() 

3251 ) 

3252 matches_with_reasons: Final = attachment_registry.get_attached_policies_with_reasons( 

3253 context, PolicyMatcher.policy_applies(context, policies_override) 

3254 ) 

3255 matching_policy_names: Final = [m["policy_name"] for m in matches_with_reasons] 

3256 policy_reasons: Final = {m["policy_name"]: m["matched_via"] for m in matches_with_reasons} 

3257 

3258 verbose_proxy_logger.debug("Policy engine: matched policies via attachments: %s", matching_policy_names) 

3259 

3260 # Combine attachment-based policies with dynamic request body policies 

3261 request_body_policies_list: Final = ( 

3262 tuple(request_body_policies) if request_body_policies and isinstance(request_body_policies, list) else () 

3263 ) 

3264 all_policy_names: Final = tuple(dict.fromkeys((*matching_policy_names, *request_body_policies_list))) 

3265 if request_body_policies_list: 3265 ↛ 3266line 3265 didn't jump to line 3266 because the condition on line 3265 was never true

3266 verbose_proxy_logger.debug("Policy engine: added dynamic policies from request body: %s", request_body_policies) 

3267 

3268 if not all_policy_names: 

3269 return [], {} 

3270 

3271 # Filter to only policies whose conditions match the context 

3272 applied_policy_names: Final = PolicyMatcher.get_policies_with_matching_conditions( 

3273 policy_names=list(all_policy_names), 

3274 context=context, 

3275 policies=policies_override, 

3276 ) 

3277 

3278 verbose_proxy_logger.debug("Policy engine: applied policies (conditions matched): %s", applied_policy_names) 

3279 

3280 # Track applied policies in metadata for response headers 

3281 for policy_name in applied_policy_names: 

3282 add_policy_to_applied_policies_header(request_data=data, policy_name=policy_name) 

3283 

3284 # Track policy attribution sources for x-litellm-policy-sources header 

3285 applied_reasons: Final = {name: policy_reasons[name] for name in applied_policy_names if name in policy_reasons} 

3286 add_policy_sources_to_metadata(request_data=data, policy_sources=applied_reasons) 

3287 

3288 return applied_policy_names, policy_reasons 

3289 

3290 

3291def _apply_resolved_guardrails_to_metadata( 

3292 data: dict, 

3293 metadata_variable_name: str, 

3294 context: "PolicyMatchContext", 

3295 policy_names: list[str] | None = None, 

3296 policies: dict[str, "Policy"] | None = None, 

3297) -> None: 

3298 """Apply resolved guardrails and pipelines to request metadata.""" 

3299 from litellm._logging import verbose_proxy_logger 

3300 from litellm.proxy.policy_engine.policy_resolver import PolicyResolver 

3301 

3302 # Resolve guardrails from matching policies 

3303 resolved_guardrails: Final = PolicyResolver.resolve_guardrails_for_context( 

3304 context=context, 

3305 policies=policies, 

3306 policy_names=policy_names, 

3307 ) 

3308 

3309 verbose_proxy_logger.debug("Policy engine: resolved guardrails: %s", resolved_guardrails) 

3310 

3311 # Resolve pipelines from matching policies 

3312 pipelines: Final = PolicyResolver.resolve_pipelines_for_context( 

3313 context=context, 

3314 policies=policies, 

3315 policy_names=policy_names, 

3316 ) 

3317 

3318 # Add resolved guardrails to request metadata 

3319 if metadata_variable_name not in data: 3319 ↛ 3320line 3319 didn't jump to line 3320 because the condition on line 3319 was never true

3320 data[metadata_variable_name] = {} 

3321 

3322 # Record the pipelines and the guardrails they step; the hook loops skip those per pipeline mode 

3323 if pipelines: 3323 ↛ 3324line 3323 didn't jump to line 3324 because the condition on line 3323 was never true

3324 pipeline_managed_guardrails: Final = PolicyResolver.get_pipeline_managed_guardrails(pipelines) 

3325 data[metadata_variable_name]["_guardrail_pipelines"] = pipelines 

3326 data[metadata_variable_name]["_pipeline_managed_guardrails"] = pipeline_managed_guardrails 

3327 verbose_proxy_logger.debug( 

3328 "Policy engine: resolved %s pipeline(s), managed guardrails: %s", 

3329 len(pipelines), 

3330 pipeline_managed_guardrails, 

3331 ) 

3332 

3333 if not resolved_guardrails and not pipelines: 

3334 return 

3335 

3336 existing_guardrails: Final = data[metadata_variable_name].get("guardrails", []) 

3337 existing_guardrails_list: Final = existing_guardrails if isinstance(existing_guardrails, list) else [] 

3338 

3339 # Combine existing guardrails with policy-resolved guardrails (no duplicates) 

3340 combined: Final = list(dict.fromkeys((*existing_guardrails_list, *resolved_guardrails))) 

3341 data[metadata_variable_name]["guardrails"] = combined 

3342 

3343 verbose_proxy_logger.debug("Policy engine: added guardrails to request metadata: %s", combined) 

3344 

3345 

3346async def add_guardrails_from_policy_engine( 

3347 data: dict, 

3348 metadata_variable_name: str, 

3349 user_api_key_dict: UserAPIKeyAuth, 

3350) -> None: 

3351 """ 

3352 Add guardrails from the policy engine based on request context. 

3353 

3354 This function: 

3355 1. Extracts "policies" from request body (if present) for dynamic policy application 

3356 2. Supports policy_<uuid> in policies to execute a specific version (e.g. published) 

3357 3. Gets matching policies based on team_alias, key_alias, and model (via attachments) 

3358 4. Combines dynamic policies with attachment-based policies 

3359 5. Resolves guardrails from all policies (including inheritance) 

3360 6. Adds guardrails to request metadata 

3361 7. Tracks applied policies in metadata for response headers 

3362 8. Removes "policies" from request body so it's not forwarded to LLM provider 

3363 

3364 Args: 

3365 data: The request data to update 

3366 metadata_variable_name: The name of the metadata field in data 

3367 user_api_key_dict: The user's API key authentication info 

3368 """ 

3369 from litellm._logging import verbose_proxy_logger 

3370 from litellm.proxy.common_utils.http_parsing_utils import get_tags_from_request_body 

3371 from litellm.proxy.policy_engine.policy_registry import get_policy_registry 

3372 from litellm.types.proxy.policy_engine import PolicyMatchContext 

3373 

3374 # Extract dynamic policies from request body (if present) 

3375 request_body_policies_raw: Final = data.pop("policies", None) 

3376 

3377 registry: Final = get_policy_registry() 

3378 verbose_proxy_logger.debug( 

3379 "Policy engine: registry initialized=%s, policy_count=%s", 

3380 registry.is_initialized(), 

3381 len(registry.get_all_policies()), 

3382 ) 

3383 if not registry.is_initialized(): 3383 ↛ 3384line 3383 didn't jump to line 3384 because the condition on line 3383 was never true

3384 verbose_proxy_logger.debug("Policy engine not initialized, skipping policy matching") 

3385 return 

3386 

3387 # Extract tags and build context 

3388 all_tags: Final = get_tags_from_request_body(data) or None 

3389 _team_alias: Final = user_api_key_dict.team_alias 

3390 _key_alias: Final = user_api_key_dict.key_alias 

3391 context: Final = PolicyMatchContext( 

3392 team_alias=_team_alias if isinstance(_team_alias, str) else None, 

3393 key_alias=_key_alias if isinstance(_key_alias, str) else None, 

3394 model=data.get("model"), 

3395 tags=all_tags, 

3396 ) 

3397 

3398 verbose_proxy_logger.debug( 

3399 "Policy engine: matching policies for context team_alias=%s, key_alias=%s, model=%s, tags=%s", 

3400 context.team_alias, 

3401 context.key_alias, 

3402 context.model, 

3403 context.tags, 

3404 ) 

3405 

3406 # Separate policy names from policy version IDs (policy_<uuid>) 

3407 request_body_names: Final[list[str]] = [] 

3408 request_body_version_ids: Final[list[str]] = [] 

3409 if request_body_policies_raw and isinstance(request_body_policies_raw, list): 3409 ↛ 3410line 3409 didn't jump to line 3410 because the condition on line 3409 was never true

3410 for item in request_body_policies_raw: 

3411 if not isinstance(item, str): 

3412 continue 

3413 if _is_policy_version_id(item): 

3414 policy_id = _extract_policy_id(item) 

3415 if policy_id: 

3416 request_body_version_ids.append(policy_id) 

3417 else: 

3418 request_body_names.append(item) 

3419 

3420 # Resolve policy versions by ID from in-memory cache (populated by sync job; no DB in hot path) 

3421 merged_policies: Final[dict[str, Policy]] = dict(registry.get_all_policies()) 

3422 fetched_policy_names: Final[list[str]] = [] 

3423 for policy_id in request_body_version_ids: 3423 ↛ 3424line 3423 didn't jump to line 3424 because the loop on line 3423 never started

3424 result = registry.get_policy_by_id_for_request(policy_id=policy_id) 

3425 if result is not None: 

3426 pname, policy = result 

3427 merged_policies[pname] = policy 

3428 fetched_policy_names.append(pname) 

3429 verbose_proxy_logger.debug("Policy engine: loaded version by ID policy_%s -> %s", policy_id, pname) 

3430 else: 

3431 verbose_proxy_logger.debug("Policy engine: policy version %s not found in cache, skipping", policy_id) 

3432 

3433 # Build request body list: names + policy names from fetched versions 

3434 request_body_policies: Final = request_body_names + fetched_policy_names 

3435 

3436 # Match and track policies (with merged_policies when we have version overrides) 

3437 applied_policy_names, _ = _match_and_track_policies( 

3438 data, 

3439 context, 

3440 request_body_policies, 

3441 policies_override=merged_policies if request_body_version_ids else None, 

3442 ) 

3443 

3444 # Resolve and apply guardrails. Use applied_policy_names so request-body policies 

3445 # (names + version IDs) are included. Use merged_policies when we have version overrides. 

3446 _apply_resolved_guardrails_to_metadata( 

3447 data, 

3448 metadata_variable_name, 

3449 context, 

3450 policy_names=applied_policy_names if applied_policy_names else None, 

3451 policies=merged_policies if request_body_version_ids else None, 

3452 ) 

3453 

3454 

3455_ANTHROPIC_API_HEADER_PROVIDERS: Final = ",".join( 

3456 ( 

3457 LlmProviders.ANTHROPIC.value, 

3458 LlmProviders.BEDROCK.value, 

3459 LlmProviders.BEDROCK_MANTLE.value, 

3460 LlmProviders.VERTEX_AI.value, 

3461 ) 

3462) 

3463_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS: Final = LlmProviders.ANTHROPIC.value 

3464 

3465 

3466def add_provider_specific_headers_to_request( 

3467 data: dict, 

3468 headers: dict, 

3469): 

3470 from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key 

3471 

3472 anthropic_api_headers: Final = {header: headers[header] for header in ANTHROPIC_API_HEADERS if header in headers} 

3473 anthropic_oauth_credential_headers: Final = { 

3474 header: value 

3475 for header, value in headers.items() 

3476 if header.lower() == "authorization" and is_anthropic_oauth_key(value) 

3477 } 

3478 

3479 scoped_headers: Final = [ 

3480 ProviderSpecificHeader(custom_llm_provider=providers, extra_headers=extra_headers) 

3481 for providers, extra_headers in ( 

3482 (_ANTHROPIC_API_HEADER_PROVIDERS, anthropic_api_headers), 

3483 (_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS, anthropic_oauth_credential_headers), 

3484 ) 

3485 if extra_headers 

3486 ] 

3487 

3488 if scoped_headers: 3488 ↛ 3489line 3488 didn't jump to line 3489 because the condition on line 3488 was never true

3489 data["provider_specific_header"] = scoped_headers[0] if len(scoped_headers) == 1 else scoped_headers 

3490 

3491 

3492def _add_otel_traceparent_to_data(data: dict, request: Request): 

3493 from litellm.proxy.proxy_server import open_telemetry_logger 

3494 

3495 if data is None: 3495 ↛ 3496line 3495 didn't jump to line 3496 because the condition on line 3495 was never true

3496 return 

3497 if open_telemetry_logger is None: 3497 ↛ 3502line 3497 didn't jump to line 3502 because the condition on line 3497 was always true

3498 # if user is not use OTEL don't send extra_headers 

3499 # relevant issue: https://github.com/BerriAI/litellm/issues/4448 

3500 return 

3501 

3502 if litellm.forward_traceparent_to_llm_provider is True: 

3503 if request.headers: 

3504 if "traceparent" in request.headers: 

3505 # we want to forward this to the LLM Provider 

3506 # Relevant issue: https://github.com/BerriAI/litellm/issues/4419 

3507 # pass this in extra_headers 

3508 if "extra_headers" not in data: 

3509 data["extra_headers"] = {} 

3510 _exra_headers: Final = data["extra_headers"] 

3511 if "traceparent" not in _exra_headers: 

3512 _exra_headers["traceparent"] = request.headers["traceparent"]