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

1import json 

2from datetime import datetime 

3from types import MappingProxyType 

4from typing import Any, Final 

5from urllib.parse import urlparse 

6 

7import httpx 

8 

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 

16 

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 

51 

52cohere_passthrough_logging_handler: Final = CoherePassthroughLoggingHandler() 

53 

54 

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 "" 

65 

66 

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",) 

89 

90 # Anthropic 

91 self.TRACKED_ANTHROPIC_ROUTES = ["/messages", "/v1/messages/batches"] 

92 

93 # Cohere 

94 self.TRACKED_COHERE_ROUTES = ["/v2/chat", "/v1/embed"] 

95 self.assemblyai_passthrough_logging_handler = AssemblyAIPassthroughLoggingHandler() 

96 

97 # Langfuse 

98 self.TRACKED_LANGFUSE_ROUTES = ["/langfuse/"] 

99 

100 # Gemini 

101 self.TRACKED_GEMINI_ROUTES = [ 

102 "generateContent", 

103 "streamGenerateContent", 

104 "predictLongRunning", 

105 ] 

106 

107 # Cursor Cloud Agents 

108 self.TRACKED_CURSOR_ROUTES = [ 

109 "/v0/agents", 

110 "/v0/me", 

111 "/v0/models", 

112 "/v0/repositories", 

113 ] 

114 

115 # Vertex AI Live API WebSocket 

116 self.TRACKED_VERTEX_AI_LIVE_ROUTES = ["/vertex_ai/live"] 

117 

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 

121 

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 ) 

150 

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 

170 

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 ) 

215 

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 ) 

237 

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"] 

252 

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 ) 

272 

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 

303 

304 elif self.is_azure_speech_route(custom_llm_provider): 

305 from .llm_provider_handlers.azure_speech_passthrough_logging_handler import ( 

306 AzureSpeechPassthroughLoggingHandler, 

307 ) 

308 

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 ) 

343 

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"] 

359 

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 ) 

364 

365 vertex_ai_live_handler: Final = VertexAILivePassthroughLoggingHandler() 

366 

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 [] 

369 

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 ) 

379 

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 

408 

409 return_dict["kwargs"] = kwargs 

410 return return_dict 

411 

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 ) 

501 

502 kwargs = self._set_cost_per_request( 

503 logging_obj=logging_obj, 

504 passthrough_logging_payload=passthrough_logging_payload, 

505 kwargs=kwargs, 

506 ) 

507 

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 ) 

518 

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) 

525 

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 

531 

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 

540 

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 

546 

547 def is_comprehend_medical_route(self, custom_llm_provider: str | None) -> bool: 

548 return custom_llm_provider == "comprehendmedical" 

549 

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) 

552 

553 def is_azure_speech_route(self, custom_llm_provider: str | None) -> bool: 

554 return custom_llm_provider == AZURE_SPEECH_CUSTOM_LLM_PROVIDER 

555 

556 def is_transcribe_route(self, custom_llm_provider: str | None) -> bool: 

557 return custom_llm_provider == TRANSCRIBE_CUSTOM_LLM_PROVIDER 

558 

559 def is_typesafe_route(self, custom_llm_provider: str | None) -> bool: 

560 return custom_llm_provider == "typesafe" 

561 

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") 

564 

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 

571 

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 

580 

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 

594 

595 def is_openai_route(self, url_route: str): 

596 """Check if the URL route is an OpenAI API route. 

597 

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 ) 

607 

608 return _is_openai_compatible_url(url_route) 

609 

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 

620 

621 def _is_supported_openai_endpoint(self, url_route: str) -> bool: 

622 """Check if the OpenAI endpoint is supported by the passthrough logging handler. 

623 

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 ) 

635 

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 ) 

643 

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 

652 

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. 

655 

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 

668 

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") 

672 

673 return kwargs