Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/pass_through_endpoints/success_handler.py: 18%
226 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1import json
2from datetime import datetime
3from types import MappingProxyType
4from typing import Any, Final
5from urllib.parse import urlparse
7import httpx
9from litellm.constants import AZURE_SPEECH_CUSTOM_LLM_PROVIDER
10from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
11from litellm.proxy._types import PassThroughEndpointLoggingResultValues
12from litellm.types.passthrough_endpoints.pass_through_endpoints import (
13 PassthroughStandardLoggingPayload,
14)
15from litellm.types.utils import StandardPassThroughResponseObject
17from .llm_provider_handlers.anthropic_passthrough_logging_handler import (
18 AnthropicPassthroughLoggingHandler,
19)
20from .llm_provider_handlers.assembly_passthrough_logging_handler import (
21 AssemblyAIPassthroughLoggingHandler,
22)
23from .llm_provider_handlers.cohere_passthrough_logging_handler import (
24 CoherePassthroughLoggingHandler,
25)
26from .llm_provider_handlers.cursor_passthrough_logging_handler import (
27 CursorPassthroughLoggingHandler,
28)
29from .llm_provider_handlers.deepgram_listen_passthrough_logging_handler import (
30 DeepgramListenPassthroughLoggingHandler,
31)
32from .llm_provider_handlers.fal_ai_passthrough_logging_handler import (
33 FalAIPassthroughLoggingHandler,
34)
35from .llm_provider_handlers.gemini_passthrough_logging_handler import (
36 GeminiPassthroughLoggingHandler,
37)
38from .llm_provider_handlers.tinyfish_passthrough_logging_handler import (
39 TinyFishPassthroughLoggingHandler,
40 is_tinyfish_agent_url,
41)
42from .llm_provider_handlers.transcribe_passthrough_logging_handler import (
43 TRANSCRIBE_CUSTOM_LLM_PROVIDER,
44 PassThroughLogDispatch,
45 TranscribePassthroughLoggingHandler,
46)
47from .llm_provider_handlers.vertex_passthrough_logging_handler import (
48 VertexPassthroughLoggingHandler,
49)
50from .upstream_usage_headers import has_upstream_reported_usage
52cohere_passthrough_logging_handler: Final = CoherePassthroughLoggingHandler()
55def _safe_response_text(httpx_response: httpx.Response) -> str:
56 """
57 Streamed passthrough responses are relayed to the client without being read
58 into memory, so accessing .text on them raises ResponseNotRead. Their body is
59 intentionally uninspected; log an empty string instead of failing the row.
60 """
61 try:
62 return httpx_response.text
63 except httpx.ResponseNotRead:
64 return ""
67class PassThroughEndpointLogging:
68 def __init__(
69 self,
70 transcribe_handler: TranscribePassthroughLoggingHandler | None = None,
71 log_dispatch: PassThroughLogDispatch | None = None,
72 ):
73 self.transcribe_passthrough_logging_handler: Final = (
74 transcribe_handler if transcribe_handler is not None else TranscribePassthroughLoggingHandler()
75 )
76 self._injected_log_dispatch: Final = log_dispatch
77 self.TRACKED_VERTEX_METHOD_ROUTES = (
78 "generateContent",
79 "streamGenerateContent",
80 "predict",
81 "rawPredict",
82 "streamRawPredict",
83 "search",
84 "predictLongRunning",
85 "embedContent",
86 "batchEmbedContents",
87 )
88 self.TRACKED_VERTEX_RESOURCE_ROUTES = ("batchPredictionJobs",)
90 # Anthropic
91 self.TRACKED_ANTHROPIC_ROUTES = ["/messages", "/v1/messages/batches"]
93 # Cohere
94 self.TRACKED_COHERE_ROUTES = ["/v2/chat", "/v1/embed"]
95 self.assemblyai_passthrough_logging_handler = AssemblyAIPassthroughLoggingHandler()
97 # Langfuse
98 self.TRACKED_LANGFUSE_ROUTES = ["/langfuse/"]
100 # Gemini
101 self.TRACKED_GEMINI_ROUTES = [
102 "generateContent",
103 "streamGenerateContent",
104 "predictLongRunning",
105 ]
107 # Cursor Cloud Agents
108 self.TRACKED_CURSOR_ROUTES = [
109 "/v0/agents",
110 "/v0/me",
111 "/v0/models",
112 "/v0/repositories",
113 ]
115 # Vertex AI Live API WebSocket
116 self.TRACKED_VERTEX_AI_LIVE_ROUTES = ["/vertex_ai/live"]
118 @property
119 def _log_dispatch(self) -> PassThroughLogDispatch:
120 return self._injected_log_dispatch if self._injected_log_dispatch is not None else self._handle_logging
122 async def _handle_logging(
123 self,
124 logging_obj: LiteLLMLoggingObj,
125 standard_logging_response_object: StandardPassThroughResponseObject
126 | PassThroughEndpointLoggingResultValues
127 | dict,
128 result: str,
129 start_time: datetime,
130 end_time: datetime,
131 cache_hit: bool,
132 **kwargs,
133 ):
134 """Log pass-through success via the shared async dispatch path."""
135 # Always reached from pass_through_async_success_handler, which runs in
136 # an async context. call_type is "pass_through_endpoint" here, so the
137 # passthrough guard in dispatch_success_handlers already forces the
138 # async handler to run; pass prefer_async_handlers explicitly to match
139 # the streaming sibling (_route_streaming_logging_to_handler) and keep
140 # async-only loggers (e.g. the proxy spend logger) firing regardless of
141 # how the call-type classification evolves.
142 await logging_obj.dispatch_success_handlers(
143 result=(json.dumps(result) if isinstance(result, dict) else standard_logging_response_object),
144 start_time=start_time,
145 end_time=end_time,
146 cache_hit=False,
147 prefer_async_handlers=True,
148 **kwargs,
149 )
151 def normalize_llm_passthrough_logging_payload(
152 self,
153 httpx_response: httpx.Response,
154 response_body: dict | list[dict[str, object]] | None,
155 request_body: dict,
156 logging_obj: LiteLLMLoggingObj,
157 url_route: str,
158 result: str,
159 start_time: datetime,
160 end_time: datetime,
161 cache_hit: bool,
162 custom_llm_provider: str | None = None,
163 **kwargs,
164 ):
165 return_dict: Final = {
166 "standard_logging_response_object": None,
167 "kwargs": kwargs,
168 }
169 standard_logging_response_object: Any | None = None
171 if self.is_gemini_route(url_route, custom_llm_provider):
172 gemini_passthrough_logging_handler_result = GeminiPassthroughLoggingHandler.gemini_passthrough_handler(
173 httpx_response=httpx_response,
174 response_body=response_body if isinstance(response_body, dict) else {},
175 logging_obj=logging_obj,
176 url_route=url_route,
177 result=result,
178 start_time=start_time,
179 end_time=end_time,
180 cache_hit=cache_hit,
181 request_body=request_body,
182 **kwargs,
183 )
184 standard_logging_response_object = gemini_passthrough_logging_handler_result["result"]
185 kwargs = gemini_passthrough_logging_handler_result["kwargs"]
186 elif self.is_vertex_route(url_route):
187 vertex_passthrough_logging_handler_result = VertexPassthroughLoggingHandler.vertex_passthrough_handler(
188 httpx_response=httpx_response,
189 logging_obj=logging_obj,
190 url_route=url_route,
191 result=result,
192 start_time=start_time,
193 end_time=end_time,
194 cache_hit=cache_hit,
195 request_body=request_body,
196 **kwargs,
197 )
198 standard_logging_response_object = vertex_passthrough_logging_handler_result["result"]
199 kwargs = vertex_passthrough_logging_handler_result["kwargs"]
200 elif self.is_anthropic_route(url_route):
201 anthropic_passthrough_logging_handler_result: Final = (
202 AnthropicPassthroughLoggingHandler.anthropic_passthrough_handler(
203 httpx_response=httpx_response,
204 response_body=response_body if isinstance(response_body, dict) else {},
205 logging_obj=logging_obj,
206 url_route=url_route,
207 result=result,
208 start_time=start_time,
209 end_time=end_time,
210 cache_hit=cache_hit,
211 request_body=request_body,
212 **kwargs,
213 )
214 )
216 standard_logging_response_object = anthropic_passthrough_logging_handler_result["result"]
217 kwargs = anthropic_passthrough_logging_handler_result["kwargs"]
218 elif self.is_cohere_route(url_route):
219 cohere_passthrough_logging_handler_result = cohere_passthrough_logging_handler.cohere_passthrough_handler(
220 httpx_response=httpx_response,
221 response_body=response_body if isinstance(response_body, dict) else {},
222 logging_obj=logging_obj,
223 url_route=url_route,
224 result=result,
225 start_time=start_time,
226 end_time=end_time,
227 cache_hit=cache_hit,
228 request_body=request_body,
229 **kwargs,
230 )
231 standard_logging_response_object = cohere_passthrough_logging_handler_result["result"]
232 kwargs = cohere_passthrough_logging_handler_result["kwargs"]
233 elif self.is_openai_route(url_route) and self._is_supported_openai_endpoint(url_route):
234 from .llm_provider_handlers.openai_passthrough_logging_handler import (
235 OpenAIPassthroughLoggingHandler,
236 )
238 openai_passthrough_logging_handler_result = OpenAIPassthroughLoggingHandler.openai_passthrough_handler(
239 httpx_response=httpx_response,
240 response_body=response_body if isinstance(response_body, dict) else {},
241 logging_obj=logging_obj,
242 url_route=url_route,
243 result=result,
244 start_time=start_time,
245 end_time=end_time,
246 cache_hit=cache_hit,
247 request_body=request_body,
248 **kwargs,
249 )
250 standard_logging_response_object = openai_passthrough_logging_handler_result["result"]
251 kwargs = openai_passthrough_logging_handler_result["kwargs"]
253 elif self.is_cursor_route(url_route, custom_llm_provider):
254 cursor_passthrough_logging_handler_result = CursorPassthroughLoggingHandler.cursor_passthrough_handler(
255 httpx_response=httpx_response,
256 response_body=response_body if isinstance(response_body, dict) else {},
257 logging_obj=logging_obj,
258 url_route=url_route,
259 result=result,
260 start_time=start_time,
261 end_time=end_time,
262 cache_hit=cache_hit,
263 request_body=request_body,
264 **kwargs,
265 )
266 standard_logging_response_object = cursor_passthrough_logging_handler_result["result"]
267 kwargs = cursor_passthrough_logging_handler_result["kwargs"]
268 elif self.is_comprehend_medical_route(custom_llm_provider):
269 from .llm_provider_handlers.comprehend_medical_passthrough_logging_handler import (
270 ComprehendMedicalPassthroughLoggingHandler,
271 )
273 comprehend_medical_handler_result: Final = (
274 ComprehendMedicalPassthroughLoggingHandler.comprehend_medical_passthrough_handler(
275 httpx_response=httpx_response,
276 logging_obj=logging_obj,
277 url_route=url_route,
278 result=result,
279 start_time=start_time,
280 end_time=end_time,
281 cache_hit=cache_hit,
282 request_body=request_body,
283 **kwargs,
284 )
285 )
286 standard_logging_response_object = comprehend_medical_handler_result["result"] # rebind-ok: elif-chain
287 kwargs = comprehend_medical_handler_result["kwargs"] # rebind-ok: elif-chain contract
288 elif self.is_tinyfish_route(url_route, custom_llm_provider):
289 tinyfish_handler_result: Final = TinyFishPassthroughLoggingHandler.tinyfish_passthrough_handler(
290 httpx_response=httpx_response,
291 response_body=response_body if isinstance(response_body, dict) else None,
292 logging_obj=logging_obj,
293 url_route=url_route,
294 result=result,
295 start_time=start_time,
296 end_time=end_time,
297 cache_hit=cache_hit,
298 request_body=request_body,
299 **kwargs,
300 )
301 standard_logging_response_object = tinyfish_handler_result["result"] # rebind-ok: elif-chain
302 kwargs = tinyfish_handler_result["kwargs"] # rebind-ok: elif-chain contract
304 elif self.is_azure_speech_route(custom_llm_provider):
305 from .llm_provider_handlers.azure_speech_passthrough_logging_handler import (
306 AzureSpeechPassthroughLoggingHandler,
307 )
309 azure_speech_handler_result: Final = AzureSpeechPassthroughLoggingHandler.azure_speech_passthrough_handler(
310 httpx_response=httpx_response,
311 response_body=response_body,
312 logging_obj=logging_obj,
313 url_route=url_route,
314 result=result,
315 start_time=start_time,
316 end_time=end_time,
317 cache_hit=cache_hit,
318 request_body=request_body,
319 **kwargs,
320 )
321 standard_logging_response_object = azure_speech_handler_result["result"] # rebind-ok: elif-chain
322 kwargs = azure_speech_handler_result["kwargs"] # rebind-ok: elif-chain contract
323 elif self.is_transcribe_route(custom_llm_provider):
324 transcribe_handler_result: Final = TranscribePassthroughLoggingHandler.transcribe_passthrough_handler(
325 httpx_response=httpx_response,
326 logging_obj=logging_obj,
327 url_route=url_route,
328 result=result,
329 start_time=start_time,
330 end_time=end_time,
331 cache_hit=cache_hit,
332 request_body=request_body,
333 **kwargs,
334 )
335 standard_logging_response_object = transcribe_handler_result["result"] # rebind-ok: elif-chain
336 kwargs = transcribe_handler_result["kwargs"] # rebind-ok: elif-chain contract
337 elif self.is_typesafe_route(custom_llm_provider) or self.is_openrouter_decisions_route(
338 url_route, custom_llm_provider
339 ):
340 from .llm_provider_handlers.typesafe_passthrough_logging_handler import (
341 TypeSafePassthroughLoggingHandler,
342 )
344 typesafe_handler_result: Final = TypeSafePassthroughLoggingHandler.typesafe_passthrough_handler(
345 httpx_response=httpx_response,
346 response_body=response_body if isinstance(response_body, dict) else MappingProxyType({}),
347 logging_obj=logging_obj,
348 url_route=url_route,
349 result=result,
350 start_time=start_time,
351 end_time=end_time,
352 cache_hit=cache_hit,
353 request_body=request_body,
354 custom_llm_provider=custom_llm_provider or "",
355 **kwargs,
356 )
357 standard_logging_response_object = typesafe_handler_result["result"]
358 kwargs = typesafe_handler_result["kwargs"]
360 elif self.is_vertex_ai_live_route(url_route):
361 from .llm_provider_handlers.vertex_ai_live_passthrough_logging_handler import (
362 VertexAILivePassthroughLoggingHandler,
363 )
365 vertex_ai_live_handler: Final = VertexAILivePassthroughLoggingHandler()
367 # For WebSocket responses, response_body should be a list of messages
368 websocket_messages: Final[list[dict[str, Any]]] = response_body if isinstance(response_body, list) else []
370 vertex_ai_live_handler_result: Final = vertex_ai_live_handler.vertex_ai_live_passthrough_handler(
371 websocket_messages=websocket_messages,
372 logging_obj=logging_obj,
373 url_route=url_route,
374 start_time=start_time,
375 end_time=end_time,
376 request_body=request_body,
377 **kwargs,
378 )
380 standard_logging_response_object = vertex_ai_live_handler_result["result"]
381 kwargs = vertex_ai_live_handler_result["kwargs"]
382 elif DeepgramListenPassthroughLoggingHandler.is_deepgram_listen_route(url_route):
383 deepgram_handler_result: Final = (
384 DeepgramListenPassthroughLoggingHandler().deepgram_listen_passthrough_handler(
385 websocket_messages=tuple(
386 message
387 for message in (response_body if isinstance(response_body, list) else ())
388 if isinstance(message, dict)
389 ),
390 logging_obj=logging_obj,
391 upstream_url=str(httpx_response.request.url),
392 kwargs=kwargs,
393 )
394 )
395 standard_logging_response_object = deepgram_handler_result["result"] # rebind-ok: elif-chain
396 kwargs = deepgram_handler_result["kwargs"] # rebind-ok: elif-chain contract
397 elif FalAIPassthroughLoggingHandler.is_fal_ai_route(url_route, custom_llm_provider):
398 fal_ai_handler_result: Final = FalAIPassthroughLoggingHandler().fal_ai_passthrough_handler(
399 response_body=response_body if isinstance(response_body, dict) else MappingProxyType({}),
400 request_body=request_body,
401 logging_obj=logging_obj,
402 url_route=url_route,
403 kwargs=kwargs,
404 )
405 standard_logging_response_object = fal_ai_handler_result["result"] # rebind-ok: elif-chain
406 kwargs = fal_ai_handler_result["kwargs"] # rebind-ok: elif-chain contract
407 return_dict["standard_logging_response_object"] = standard_logging_response_object
409 return_dict["kwargs"] = kwargs
410 return return_dict
412 async def pass_through_async_success_handler(
413 self,
414 httpx_response: httpx.Response,
415 response_body: dict | list[dict[str, object]] | None,
416 logging_obj: LiteLLMLoggingObj,
417 url_route: str,
418 result: str,
419 start_time: datetime,
420 end_time: datetime,
421 cache_hit: bool,
422 request_body: dict,
423 passthrough_logging_payload: PassthroughStandardLoggingPayload,
424 custom_llm_provider: str | None = None,
425 **kwargs,
426 ):
427 standard_logging_response_object: PassThroughEndpointLoggingResultValues | None = None
428 logging_obj.model_call_details["passthrough_logging_payload"] = passthrough_logging_payload
429 if self.is_tinyfish_route(url_route, custom_llm_provider):
430 # polls and cancels never write spend rows; run-async bills once from the background poller
431 if not TinyFishPassthroughLoggingHandler.should_log_request(httpx_response.request.method, url_route):
432 return
433 if TinyFishPassthroughLoggingHandler.is_run_async_route(url_route):
434 TinyFishPassthroughLoggingHandler.start_async_run_billing(
435 response_body=response_body if isinstance(response_body, dict) else None,
436 logging_obj=logging_obj,
437 result=result,
438 start_time=start_time,
439 cache_hit=cache_hit,
440 **kwargs,
441 )
442 return
443 if self.is_assemblyai_route(url_route) and not self.is_azure_speech_route(custom_llm_provider):
444 if AssemblyAIPassthroughLoggingHandler._should_log_request(httpx_response.request.method) is not True:
445 return
446 self.assemblyai_passthrough_logging_handler.assemblyai_passthrough_logging_handler(
447 httpx_response=httpx_response,
448 response_body=response_body if isinstance(response_body, dict) else {},
449 logging_obj=logging_obj,
450 url_route=url_route,
451 result=result,
452 start_time=start_time,
453 end_time=end_time,
454 cache_hit=cache_hit,
455 **kwargs,
456 )
457 return
458 elif self.is_langfuse_route(url_route):
459 # Don't log langfuse pass-through requests
460 return
461 elif self.is_transcribe_route(custom_llm_provider) and TranscribePassthroughLoggingHandler.is_priced_job_start(
462 httpx_response
463 ):
464 self.transcribe_passthrough_logging_handler.schedule_priced_job_logging(
465 httpx_response=httpx_response,
466 response_body=response_body if isinstance(response_body, dict) else None,
467 logging_obj=logging_obj,
468 url_route=url_route,
469 result=result,
470 start_time=start_time,
471 end_time=end_time,
472 cache_hit=cache_hit,
473 request_body=request_body,
474 log=self._log_dispatch,
475 standard_pass_through_logging_payload=passthrough_logging_payload,
476 **kwargs,
477 )
478 return
479 else:
480 normalized_llm_passthrough_logging_payload: Final = self.normalize_llm_passthrough_logging_payload(
481 httpx_response=httpx_response,
482 response_body=response_body,
483 request_body=request_body,
484 logging_obj=logging_obj,
485 url_route=url_route,
486 result=result,
487 start_time=start_time,
488 end_time=end_time,
489 cache_hit=cache_hit,
490 custom_llm_provider=custom_llm_provider,
491 **kwargs,
492 )
493 standard_logging_response_object = normalized_llm_passthrough_logging_payload[
494 "standard_logging_response_object"
495 ]
496 kwargs = normalized_llm_passthrough_logging_payload["kwargs"]
497 if standard_logging_response_object is None:
498 standard_logging_response_object = StandardPassThroughResponseObject(
499 response=_safe_response_text(httpx_response)
500 )
502 kwargs = self._set_cost_per_request(
503 logging_obj=logging_obj,
504 passthrough_logging_payload=passthrough_logging_payload,
505 kwargs=kwargs,
506 )
508 await self._log_dispatch(
509 logging_obj=logging_obj,
510 standard_logging_response_object=standard_logging_response_object,
511 result=result,
512 start_time=start_time,
513 end_time=end_time,
514 cache_hit=cache_hit,
515 standard_pass_through_logging_payload=passthrough_logging_payload,
516 **kwargs,
517 )
519 def is_vertex_route(self, url_route: str) -> bool:
520 if any(f":{method}" in url_route for method in self.TRACKED_VERTEX_METHOD_ROUTES):
521 return True
522 if any(resource in url_route for resource in self.TRACKED_VERTEX_RESOURCE_ROUTES):
523 return True
524 return VertexPassthroughLoggingHandler.is_vertex_interactions_route(url_route)
526 def is_anthropic_route(self, url_route: str):
527 for route in self.TRACKED_ANTHROPIC_ROUTES:
528 if route in url_route:
529 return True
530 return False
532 def is_cohere_route(self, url_route: str) -> bool:
533 for route in self.TRACKED_COHERE_ROUTES:
534 if route not in url_route:
535 continue
536 if route == "/v1/embed" and "/v1/embeddings" in url_route:
537 continue
538 return True
539 return False
541 def is_assemblyai_route(self, url_route: str):
542 parsed_url: Final = urlparse(url_route)
543 if parsed_url.hostname == "api.assemblyai.com" or "/transcript" in parsed_url.path:
544 return True
545 return False
547 def is_comprehend_medical_route(self, custom_llm_provider: str | None) -> bool:
548 return custom_llm_provider == "comprehendmedical"
550 def is_tinyfish_route(self, url_route: str, custom_llm_provider: str | None) -> bool:
551 return custom_llm_provider == "tinyfish" or is_tinyfish_agent_url(url_route)
553 def is_azure_speech_route(self, custom_llm_provider: str | None) -> bool:
554 return custom_llm_provider == AZURE_SPEECH_CUSTOM_LLM_PROVIDER
556 def is_transcribe_route(self, custom_llm_provider: str | None) -> bool:
557 return custom_llm_provider == TRANSCRIBE_CUSTOM_LLM_PROVIDER
559 def is_typesafe_route(self, custom_llm_provider: str | None) -> bool:
560 return custom_llm_provider == "typesafe"
562 def is_openrouter_decisions_route(self, url_route: str, custom_llm_provider: str | None) -> bool:
563 return custom_llm_provider == "openrouter" and urlparse(url_route).path.endswith("/alpha/decisions")
565 def is_langfuse_route(self, url_route: str):
566 parsed_url: Final = urlparse(url_route)
567 for route in self.TRACKED_LANGFUSE_ROUTES:
568 if route in parsed_url.path:
569 return True
570 return False
572 def is_vertex_ai_live_route(self, url_route: str):
573 """Check if the URL route is a Vertex AI Live API WebSocket route."""
574 if not url_route:
575 return False
576 for route in self.TRACKED_VERTEX_AI_LIVE_ROUTES:
577 if route in url_route:
578 return True
579 return False
581 def is_cursor_route(self, url_route: str, custom_llm_provider: str | None = None):
582 """Check if the URL route is a Cursor Cloud Agents API route."""
583 if custom_llm_provider == "cursor":
584 return True
585 parsed_url: Final = urlparse(url_route)
586 if parsed_url.hostname and "api.cursor.com" in parsed_url.hostname:
587 return True
588 for route in self.TRACKED_CURSOR_ROUTES:
589 if route in url_route:
590 path = parsed_url.path if parsed_url.scheme else url_route
591 if path.startswith("/v0/"):
592 return custom_llm_provider == "cursor"
593 return False
595 def is_openai_route(self, url_route: str):
596 """Check if the URL route is an OpenAI API route.
598 Uses the URL-aware helper so that non-OpenAI Azure Cognitive Services
599 (Speech, Vision, Language, ...) sharing the `*.cognitiveservices.azure.com`
600 / `*.openai.azure.com` domains are not misclassified as OpenAI routes.
601 """
602 if not url_route:
603 return False
604 from .llm_provider_handlers.openai_passthrough_logging_handler import (
605 _is_openai_compatible_url,
606 )
608 return _is_openai_compatible_url(url_route)
610 def is_gemini_route(self, url_route: str, custom_llm_provider: str | None = None):
611 """Check if the URL route is a Gemini API route."""
612 if custom_llm_provider != "gemini":
613 return False
614 if VertexPassthroughLoggingHandler.is_interactions_route(url_route):
615 return True
616 for route in self.TRACKED_GEMINI_ROUTES:
617 if route in url_route:
618 return True
619 return False
621 def _is_supported_openai_endpoint(self, url_route: str) -> bool:
622 """Check if the OpenAI endpoint is supported by the passthrough logging handler.
624 The Responses API route is included because
625 `openai_passthrough_handler` has a dedicated `elif is_responses:`
626 branch that knows how to extract usage + cost from the
627 Responses-API on-the-wire shape. Without including it here, the
628 outer dispatch filters Responses calls out before reaching the
629 handler — the inner branch is then unreachable and Responses
630 calls land in `LiteLLM_SpendLogs` with zero tokens / zero spend.
631 """
632 from .llm_provider_handlers.openai_passthrough_logging_handler import (
633 OpenAIPassthroughLoggingHandler,
634 )
636 return (
637 OpenAIPassthroughLoggingHandler.is_openai_chat_completions_route(url_route)
638 or OpenAIPassthroughLoggingHandler.is_openai_embeddings_route(url_route)
639 or OpenAIPassthroughLoggingHandler.is_openai_image_generation_route(url_route)
640 or OpenAIPassthroughLoggingHandler.is_openai_image_editing_route(url_route)
641 or OpenAIPassthroughLoggingHandler.is_openai_responses_route(url_route)
642 )
644 def _set_cost_per_request(
645 self,
646 logging_obj: LiteLLMLoggingObj,
647 passthrough_logging_payload: PassthroughStandardLoggingPayload,
648 kwargs: dict,
649 ):
650 """
651 Helper function to set the cost per request in the logging object
653 Only set the cost per request if it's set in the passthrough logging payload.
654 If it's not set, don't set it in the logging object.
656 An upstream that prices its own requests always wins: ``cost_per_request``
657 is a flat per-request estimate for targets LiteLLM cannot price, and it
658 defaults to 0.0 on every config-defined endpoint, so honoring it here
659 would zero out the real cost the upstream reported. That holds even when
660 the reported value was unusable, where the contract records 0 rather
661 than billing an estimate the upstream just contradicted.
662 """
663 #########################################################
664 # Check if cost per request is set
665 #########################################################
666 if has_upstream_reported_usage(logging_obj):
667 return kwargs
669 if passthrough_logging_payload.get("cost_per_request") is not None:
670 kwargs["response_cost"] = passthrough_logging_payload.get("cost_per_request")
671 logging_obj.model_call_details["response_cost"] = passthrough_logging_payload.get("cost_per_request")
673 return kwargs