Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/response_api_endpoints/endpoints.py: 42%

466 statements  

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

1import asyncio 

2import contextlib 

3import json 

4import time 

5from collections.abc import AsyncIterator, Awaitable, Mapping, Sequence 

6from enum import Enum 

7from functools import partial 

8from types import MappingProxyType 

9from typing import TYPE_CHECKING, Any, Final, NamedTuple, Protocol, TypeAlias, cast, get_args 

10from uuid import uuid4 

11 

12import fastapi 

13from fastapi import APIRouter, Depends, HTTPException, Request, Response 

14from fastapi.responses import JSONResponse 

15from openai.types.responses import ResponseItemList 

16from openai.types.responses.response_create_params import ResponseInputParam 

17from pydantic import BaseModel, ConfigDict, ValidationError 

18from starlette.websockets import WebSocket, WebSocketDisconnect 

19from typing_extensions import ReadOnly, TypedDict 

20 

21from litellm._logging import verbose_proxy_logger 

22from litellm.constants import EMPTY_MAPPING 

23from litellm.integrations.custom_guardrail import ModifyResponseException 

24from litellm.llms.base_llm.guardrail_translation.utils import ( 

25 blocked_responses_api_usage as _blocked_responses_api_usage, 

26) 

27from litellm.proxy._types import * 

28from litellm.proxy.auth.user_api_key_auth import ( 

29 UserAPIKeyAuth, 

30 user_api_key_auth, 

31 user_api_key_auth_websocket, 

32) 

33from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing 

34from litellm.proxy.common_utils.http_parsing_utils import ( 

35 _read_request_body, 

36 _safe_set_request_parsed_body, 

37) 

38from litellm.proxy.route_llm_request import raise_if_required_body_param_missing 

39from litellm.types.llms.openai import ( 

40 REASONING_EFFORT, 

41 ResponsesAPIOptionalRequestParams, 

42 ResponsesAPIResponse, 

43) 

44from litellm.types.responses.main import DeleteResponseResult 

45from litellm.types.utils import TokenCountResponse 

46 

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

48 from litellm.router import Router 

49 

50router: Final = APIRouter() 

51 

52_ResponseDocSchemas: TypeAlias = dict[int | str, dict[str, object]] # fastapi's responses kwarg 

53 

54RESPONSES_API_RESPONSE_SCHEMAS: Final[_ResponseDocSchemas] = {200: {"model": ResponsesAPIResponse}} 

55RESPONSES_API_CREATE_RESPONSE_SCHEMAS: Final[_ResponseDocSchemas] = { 

56 200: { 

57 "model": ResponsesAPIResponse, 

58 "content": { 

59 "text/event-stream": {"schema": {"type": "string", "description": "Server sent events when stream=true"}} 

60 }, 

61 } 

62} 

63DELETE_RESPONSE_SCHEMAS: Final[_ResponseDocSchemas] = {200: {"model": DeleteResponseResult}} 

64RESPONSE_ITEM_LIST_SCHEMAS: Final[_ResponseDocSchemas] = {200: {"model": ResponseItemList}} 

65 

66_user_api_key_auth_dep: Final = Depends(user_api_key_auth) 

67_RESPONSES_TAGS: Final[list[str | Enum]] = ["responses"] # mutable-ok: fastapi's route signature requires list tags 

68 

69_TOOL_PAYLOAD_KEYS: Final[Mapping[str, tuple[str, ...]]] = MappingProxyType( 

70 { 

71 "custom": ("name", "description", "format"), 

72 "function": ("name", "description", "parameters", "strict"), 

73 } 

74) 

75_EMPTY_TOOL_PAYLOAD: Final[Mapping[str, object]] = MappingProxyType({}) 

76 

77 

78def _convert_tool_payload_value(key: str, value: object, *, to_chat: bool) -> object: 

79 if key != "format" or not isinstance(value, dict): 

80 return value 

81 from litellm.litellm_core_utils.prompt_templates.common_utils import ( 

82 convert_custom_tool_format_to_chat_shape, 

83 convert_custom_tool_format_to_responses_shape, 

84 ) 

85 

86 convert = convert_custom_tool_format_to_chat_shape if to_chat else convert_custom_tool_format_to_responses_shape 

87 return convert(value) 

88 

89 

90def _convert_tool_envelope(obj: object, *, to_chat: bool) -> object: 

91 if not isinstance(obj, dict): 91 ↛ 93line 91 didn't jump to line 93 because the condition on line 91 was always true

92 return obj 

93 tool_type: Final = obj.get("type") 

94 payload_keys: Final = _TOOL_PAYLOAD_KEYS.get(tool_type) 

95 if payload_keys is None: 

96 return obj 

97 nested: Final = obj.get(tool_type) 

98 nested_source: Final = nested if isinstance(nested, dict) else _EMPTY_TOOL_PAYLOAD 

99 payload: Final = { # mutable-ok: tool entries are embedded verbatim in the JSON request body 

100 key: _convert_tool_payload_value(key, nested_source[key] if key in nested_source else obj[key], to_chat=to_chat) 

101 for key in payload_keys 

102 if key in nested_source or key in obj 

103 } 

104 if "name" not in payload: 

105 return obj 

106 return {"type": tool_type, tool_type: payload} if to_chat else {"type": tool_type, **payload} # mutable-ok: same 

107 

108 

109def _normalize_tool_dialect( 

110 data: dict, *, to_chat: bool 

111) -> dict: # mutable-ok: the parsed request body contract is a plain dict 

112 tools: Final = data.get("tools") 

113 tool_choice: Final = data.get("tool_choice") 

114 normalized_tools: Final = ( 

115 [_convert_tool_envelope(tool, to_chat=to_chat) for tool in tools] if isinstance(tools, list) else tools 

116 ) 

117 normalized_choice: Final = _convert_tool_envelope(tool_choice, to_chat=to_chat) 

118 if normalized_tools == tools and normalized_choice == tool_choice: 118 ↛ 120line 118 didn't jump to line 120 because the condition on line 118 was always true

119 return data 

120 replaceable: Final = (("tools", normalized_tools), ("tool_choice", normalized_choice)) 

121 return {**data, **{key: value for key, value in replaceable if key in data}} # mutable-ok: plain body dict 

122 

123 

124def _is_chat_completions_body(data: Mapping[str, object]) -> bool: 

125 messages: Final = data.get("messages") 

126 if isinstance(messages, list) and messages: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 return True 

128 return "messages" in data and "input" not in data 

129 

130 

131_CURSOR_THINKING_SEPARATOR: Final = "-thinking-" 

132_CURSOR_FAST_SUFFIX: Final = "-fast" 

133_CURSOR_THINKING_LEVELS: Final[frozenset[str]] = frozenset(get_args(REASONING_EFFORT)) 

134 

135 

136class _CursorModelVariant(NamedTuple): 

137 base_model: str 

138 reasoning_effort: str | None 

139 

140 

141def _parse_cursor_model_variant(model: str) -> _CursorModelVariant: 

142 stripped: Final = model.removesuffix(_CURSOR_FAST_SUFFIX) 

143 base, separator, level = stripped.rpartition(_CURSOR_THINKING_SEPARATOR) 

144 if separator and base and level in _CURSOR_THINKING_LEVELS: 

145 return _CursorModelVariant(base, level) 

146 return _CursorModelVariant(stripped, None) 

147 

148 

149def _router_can_serve(model: str, llm_router: "Router | None") -> bool: 

150 if llm_router is None: 

151 return False 

152 if llm_router.is_recognized_model(model): 

153 return True 

154 if model in llm_router.team_public_model_names: 

155 return True 

156 return bool(llm_router.pattern_router.get_pattern(model)) 

157 

158 

159def _resolve_cursor_model_variant( 

160 data: dict, llm_router: "Router | None" 

161) -> dict: # mutable-ok: the parsed request body contract is a plain dict 

162 model: Final = data.get("model") 

163 if not isinstance(model, str) or _router_can_serve(model, llm_router): 163 ↛ 165line 163 didn't jump to line 165 because the condition on line 163 was always true

164 return data 

165 variant: Final = _parse_cursor_model_variant(model) 

166 if variant.base_model == model or not _router_can_serve(variant.base_model, llm_router): 

167 return data 

168 resolved: Final = {**data, "model": variant.base_model} # mutable-ok: plain body dict 

169 if variant.reasoning_effort is None: 

170 return resolved 

171 if _is_chat_completions_body(data): 

172 if "reasoning_effort" in data: 

173 return resolved 

174 return {**resolved, "reasoning_effort": variant.reasoning_effort} # mutable-ok: plain body dict 

175 reasoning: Final = data.get("reasoning") 

176 if isinstance(reasoning, dict): 

177 if reasoning.get("effort"): 

178 return resolved 

179 return {**resolved, "reasoning": {**reasoning, "effort": variant.reasoning_effort}} # mutable-ok: same 

180 return {**resolved, "reasoning": {"effort": variant.reasoning_effort}} # mutable-ok: plain body dict 

181 

182 

183async def _resolve_cursor_model_variant_before_auth(request: Request) -> None: 

184 from litellm.proxy.proxy_server import llm_router 

185 

186 try: 

187 raw_body: Final = await _read_request_body(request=request) 

188 except (json.JSONDecodeError, ProxyException): 

189 return 

190 resolved: Final = _resolve_cursor_model_variant(raw_body, llm_router) 

191 if resolved is not raw_body: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true

192 _safe_set_request_parsed_body(request=request, parsed_body=resolved) 

193 

194 

195@router.post( 

196 "/v1/responses", 

197 dependencies=[Depends(user_api_key_auth)], 

198 tags=["responses"], 

199 responses=RESPONSES_API_CREATE_RESPONSE_SCHEMAS, 

200) 

201@router.post( 

202 "/responses", 

203 dependencies=[Depends(user_api_key_auth)], 

204 tags=["responses"], 

205 responses=RESPONSES_API_CREATE_RESPONSE_SCHEMAS, 

206) 

207@router.post( 

208 "/openai/v1/responses", 

209 dependencies=[Depends(user_api_key_auth)], 

210 tags=["responses"], 

211 responses=RESPONSES_API_CREATE_RESPONSE_SCHEMAS, 

212) 

213async def responses_api( 

214 request: Request, 

215 fastapi_response: Response, 

216 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

217): 

218 """ 

219 Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses 

220 

221 Supports background mode with polling_via_cache for partial response retrieval. 

222 When background=true and polling_via_cache is enabled, returns a polling_id immediately 

223 and streams the response in the background, updating Redis cache. 

224 

225 ```bash 

226 # Normal request 

227 curl -X POST http://localhost:4000/v1/responses \ 

228 -H "Content-Type: application/json" \ 

229 -H "Authorization: Bearer sk-1234" \ 

230 -d '{ 

231 "model": "gpt-4o", 

232 "input": "Tell me about AI" 

233 }' 

234 

235 # Background request with polling 

236 curl -X POST http://localhost:4000/v1/responses \ 

237 -H "Content-Type: application/json" \ 

238 -H "Authorization: Bearer sk-1234" \ 

239 -d '{ 

240 "model": "gpt-4o", 

241 "input": "Tell me about AI", 

242 "background": true 

243 }' 

244 ``` 

245 """ 

246 from litellm.proxy.proxy_server import ( 

247 _read_request_body, 

248 general_settings, 

249 llm_router, 

250 native_background_mode, 

251 polling_cache_ttl, 

252 polling_via_cache_enabled, 

253 proxy_config, 

254 proxy_logging_obj, 

255 redis_usage_cache, 

256 select_data_generator, 

257 user_api_base, 

258 user_max_tokens, 

259 user_model, 

260 user_request_timeout, 

261 user_temperature, 

262 version, 

263 ) 

264 

265 native_data_generator: Final = partial(select_data_generator, responses_stream_errors=True) 

266 data = await _read_request_body(request=request) 

267 

268 # Check if polling via cache should be used for this request 

269 from litellm.proxy.response_polling.polling_handler import ( 

270 should_use_polling_for_request, 

271 ) 

272 

273 should_use_polling: Final = should_use_polling_for_request( 

274 background_mode=data.get("background", False), 

275 polling_via_cache_enabled=polling_via_cache_enabled, 

276 redis_cache=redis_usage_cache, 

277 model=data.get("model", ""), 

278 llm_router=llm_router, 

279 native_background_mode=native_background_mode, 

280 ) 

281 

282 # If polling is enabled, use polling mode 

283 if should_use_polling: 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true

284 from litellm.proxy.response_polling.background_streaming import ( 

285 background_streaming_task, 

286 ) 

287 from litellm.proxy.response_polling.polling_handler import ( 

288 ResponsePollingHandler, 

289 ) 

290 

291 verbose_proxy_logger.info("Starting background response with polling for model=%s", data.get("model")) 

292 

293 # Run pre-call checks (rate limits, guardrails, budget) BEFORE creating 

294 # polling ID. This ensures rate-limited requests get a synchronous 429 

295 # instead of a polling ID that immediately fails in the background task. 

296 processor = ProxyBaseLLMRequestProcessing(data=data) 

297 try: 

298 data, _logging_obj = await processor.common_processing_pre_call_logic( 

299 request=request, 

300 general_settings=general_settings, 

301 proxy_logging_obj=proxy_logging_obj, 

302 user_api_key_dict=user_api_key_dict, 

303 version=version, 

304 proxy_config=proxy_config, 

305 user_model=user_model, 

306 user_temperature=user_temperature, 

307 user_request_timeout=user_request_timeout, 

308 user_max_tokens=user_max_tokens, 

309 user_api_base=user_api_base, 

310 model=None, 

311 route_type="aresponses", 

312 llm_router=llm_router, 

313 ) 

314 raise_if_required_body_param_missing(route_type="aresponses", data=data) 

315 except Exception as e: 

316 raise await processor._handle_llm_api_exception( 

317 e=e, 

318 user_api_key_dict=user_api_key_dict, 

319 proxy_logging_obj=proxy_logging_obj, 

320 version=version, 

321 ) 

322 

323 # Initialize polling handler with configured TTL (from global config) 

324 polling_handler: Final = ResponsePollingHandler( 

325 redis_cache=redis_usage_cache, 

326 ttl=polling_cache_ttl, # Global var set at startup 

327 ) 

328 

329 # Generate polling ID 

330 polling_id: Final = ResponsePollingHandler.generate_polling_id() 

331 

332 # Create initial state in Redis 

333 initial_state: Final = await polling_handler.create_initial_state( 

334 polling_id=polling_id, 

335 request_data=data, 

336 ) 

337 

338 # Start background task to stream and update cache. 

339 # Pass pre-processed data so the background task skips pre-call logic 

340 # (rate limits, guardrails already checked above). 

341 asyncio.create_task( 

342 background_streaming_task( 

343 polling_id=polling_id, 

344 data=data.copy(), 

345 polling_handler=polling_handler, 

346 request=request, 

347 fastapi_response=fastapi_response, 

348 user_api_key_dict=user_api_key_dict, 

349 general_settings=general_settings, 

350 llm_router=llm_router, 

351 proxy_config=proxy_config, 

352 proxy_logging_obj=proxy_logging_obj, 

353 select_data_generator=native_data_generator, 

354 user_model=user_model, 

355 user_temperature=user_temperature, 

356 user_request_timeout=user_request_timeout, 

357 user_max_tokens=user_max_tokens, 

358 user_api_base=user_api_base, 

359 version=version, 

360 ) 

361 ) 

362 

363 # Return OpenAI Response object format (initial state) 

364 # https://platform.openai.com/docs/api-reference/responses/object 

365 return initial_state 

366 

367 # Normal response flow 

368 processor = ProxyBaseLLMRequestProcessing(data=data) 

369 try: 

370 response: Final = await processor.base_process_llm_request( 

371 request=request, 

372 fastapi_response=fastapi_response, 

373 user_api_key_dict=user_api_key_dict, 

374 route_type="aresponses", 

375 proxy_logging_obj=proxy_logging_obj, 

376 llm_router=llm_router, 

377 general_settings=general_settings, 

378 proxy_config=proxy_config, 

379 select_data_generator=native_data_generator, 

380 model=None, 

381 user_model=user_model, 

382 user_temperature=user_temperature, 

383 user_request_timeout=user_request_timeout, 

384 user_max_tokens=user_max_tokens, 

385 user_api_base=user_api_base, 

386 version=version, 

387 ) 

388 

389 # Store in managed objects table if background mode is enabled 

390 if data.get("background") and isinstance(response, ResponsesAPIResponse): 

391 if response.status in ["queued", "in_progress"]: 

392 from litellm_enterprise.proxy.hooks.managed_files import ( 

393 _PROXY_LiteLLMManagedFiles, 

394 ) 

395 

396 managed_files_obj: Final = cast( 

397 _PROXY_LiteLLMManagedFiles | None, 

398 proxy_logging_obj.get_proxy_hook("managed_files"), 

399 ) 

400 

401 if managed_files_obj and llm_router: 

402 try: 

403 # Get the actual deployment model_id from hidden params 

404 hidden_params: Final = getattr(response, "_hidden_params", {}) or {} 

405 model_id: Final = hidden_params.get("model_id", None) 

406 

407 if not model_id: 

408 verbose_proxy_logger.warning( 

409 "No model_id found in response hidden params for response %s, skipping managed object storage", 

410 response.id, 

411 ) 

412 raise Exception("No model_id found in response hidden params") 

413 # Store in managed objects table 

414 await managed_files_obj.store_unified_object_id( 

415 unified_object_id=response.id, 

416 file_object=response, 

417 litellm_parent_otel_span=None, 

418 model_object_id=response.id, 

419 file_purpose="response", 

420 user_api_key_dict=user_api_key_dict, 

421 ) 

422 

423 verbose_proxy_logger.info( 

424 "Stored background response %s in managed objects table with unified_id=%s", 

425 response.id, 

426 response.id, 

427 ) 

428 except Exception as e: 

429 verbose_proxy_logger.error( 

430 "Failed to store background response in managed objects table: %s", e 

431 ) 

432 

433 return response 

434 except ModifyResponseException as e: 

435 # Guardrail passthrough: return violation message in Responses API format (200) 

436 _data: Final = e.request_data 

437 await proxy_logging_obj.post_call_failure_hook( 

438 user_api_key_dict=user_api_key_dict, 

439 original_exception=e, 

440 request_data=_data, 

441 ) 

442 

443 violation_text: Final = e.message 

444 response_obj: Final = ResponsesAPIResponse( 

445 id=f"resp_{uuid4()}", 

446 object="response", 

447 created_at=int(time.time()), 

448 model=e.model or data.get("model"), 

449 output=cast(Any, [{"content": [{"type": "text", "text": violation_text}]}]), 

450 status="completed", 

451 usage=_blocked_responses_api_usage(e.original_response), 

452 ) 

453 return response_obj 

454 except Exception as e: 

455 raise await processor._handle_llm_api_exception( 

456 e=e, 

457 user_api_key_dict=user_api_key_dict, 

458 proxy_logging_obj=proxy_logging_obj, 

459 version=version, 

460 ) 

461 

462 

463@router.get( 

464 "/cursor/models", 

465 dependencies=(_user_api_key_auth_dep,), 

466 tags=_RESPONSES_TAGS, 

467) 

468@router.get( 

469 "/cursor/v1/models", 

470 dependencies=(_user_api_key_auth_dep,), 

471 tags=_RESPONSES_TAGS, 

472) 

473async def cursor_model_list( 

474 user_api_key_dict: UserAPIKeyAuth = _user_api_key_auth_dep, 

475): 

476 """ 

477 OpenAI-compatible model listing for the Cursor BYOK base URL. 

478 

479 Clients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and 

480 verify models via `GET {base}/models` (the OpenAI SDK contract). Without this 

481 route those requests fall through to the Cursor Cloud Agents passthrough, which 

482 demands a Cursor API key and 401s, so key verification silently fails before any 

483 chat request is ever sent. Delegates to the standard `/v1/models` handler. 

484 """ 

485 from litellm.proxy.proxy_server import model_list 

486 

487 return await model_list(user_api_key_dict=user_api_key_dict) 

488 

489 

490@router.post( 

491 "/cursor/chat/completions", 

492 dependencies=[ 

493 Depends(_resolve_cursor_model_variant_before_auth), 

494 Depends(user_api_key_auth), 

495 ], 

496 tags=["responses"], 

497) 

498async def cursor_chat_completions( 

499 request: Request, 

500 fastapi_response: Response, 

501 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

502): 

503 """ 

504 Cursor BYOK endpoint. Accepts both request shapes Cursor sends to its OpenAI-compatible 

505 base URL and always answers in chat completions format. 

506 

507 Cursor agent mode sends Responses API format bodies (`input`, flat tool defs, `reasoning`, 

508 custom tools) to the chat/completions path while expecting chat completions responses; 

509 those are routed through the Responses API pipeline and converted back. Genuine chat 

510 completions bodies (`messages` present) are routed through the standard chat completions 

511 pipeline, after normalizing each level of the `tools` array and `tool_choice` to the chat 

512 completions shapes OpenAI requires. Cursor mixes Responses API shapes into chat bodies 

513 per level, independently: a flat tool def (`{"type": "custom", "name": "ApplyPatch", ...}`) 

514 gets nested under `custom`, and a flat grammar format 

515 (`{"type": "grammar", "definition", "syntax"}`) gets wrapped as 

516 `{"type": "grammar", "grammar": {...}}` wherever it appears, including inside tool defs 

517 Cursor already sent pre-nested. 

518 

519 ```bash 

520 curl -X POST http://localhost:4000/cursor/chat/completions \ 

521 -H "Content-Type: application/json" \ 

522 -H "Authorization: Bearer sk-1234" \ 

523 -d '{ 

524 "model": "gpt-4o", 

525 "input": [{"role": "user", "content": "Hello"}] 

526 }' 

527 Responds back in chat completions format. 

528 ``` 

529 """ 

530 from litellm.completion_extras.litellm_responses_transformation.handler import ( 

531 responses_api_bridge, 

532 ) 

533 from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper 

534 from litellm.proxy.proxy_server import ( 

535 async_data_generator, 

536 chat_completion, 

537 general_settings, 

538 llm_router, 

539 proxy_config, 

540 proxy_logging_obj, 

541 user_api_base, 

542 user_max_tokens, 

543 user_model, 

544 user_request_timeout, 

545 user_temperature, 

546 version, 

547 ) 

548 from litellm.types.llms.openai import ResponsesAPIResponse 

549 from litellm.types.utils import ModelResponse 

550 

551 raw_body: Final = await _read_request_body(request=request) 

552 

553 if _is_chat_completions_body(raw_body): 553 ↛ 558line 553 didn't jump to line 558 because the condition on line 553 was never true

554 # Genuine chat completions body (Cursor sends these for models whose BYOK it 

555 # already fixed); delegate so behavior matches /chat/completions exactly. 

556 # Keyed on messages CONTENT, not key presence: Cursor can send a null or 

557 # empty messages stub alongside a real agent-mode input array 

558 normalized: Final = _normalize_tool_dialect(raw_body, to_chat=True) 

559 if normalized is not raw_body: 

560 _safe_set_request_parsed_body(request=request, parsed_body=normalized) 

561 return await chat_completion( 

562 request=request, 

563 fastapi_response=fastapi_response, 

564 model=None, 

565 user_api_key_dict=user_api_key_dict, 

566 ) 

567 

568 # OpenAI's Responses API rejects chat-completions-only stream_options 

569 # (Cursor sends include_usage); usage arrives via response.completed anyway. 

570 # Rebuild rather than pop: _read_request_body can return the request-scope 

571 # cached parsed-body dict itself, and removing keys from it corrupts the 

572 # cache's key snapshot so later readers get an empty body 

573 body_without_stream_options: Final = { # mutable-ok: base_process_llm_request mutates the body dict in place 

574 key: value for key, value in raw_body.items() if key != "stream_options" 

575 } 

576 

577 data: Final = _normalize_tool_dialect(body_without_stream_options, to_chat=False) 

578 

579 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

580 

581 def cursor_data_generator(response, user_api_key_dict, request_data, request=None): 

582 """ 

583 Custom generator that transforms Responses API streaming chunks to chat completion chunks. 

584 

585 This generator is used for the cursor endpoint to convert Responses API format responses 

586 to chat completion format that Cursor IDE expects. 

587 

588 Args: 

589 response: The streaming Responses API event iterator (router-wrapped or not) 

590 user_api_key_dict: User API key authentication dict 

591 request_data: Request data containing model, logging_obj, etc. 

592 request: The originating FastAPI request, forwarded for disconnect handling 

593 

594 Returns: 

595 Async generator that yields SSE-formatted chat completion chunks 

596 """ 

597 # Any async-iterable here is a Responses API event stream needing conversion. 

598 # Class-identity checks miss router-wrapped streams (e.g. 

599 # HiddenParamsAsyncIteratorWrapper around LiteLLMCompletionStreamingIterator), 

600 # which previously leaked raw Responses events to the client. 

601 if hasattr(response, "__anext__"): 

602 # Transform Responses API iterator to chat completion iterator 

603 # Cast to AsyncIterator[str] since the stream implements __aiter__/__anext__ 

604 completion_stream: Final = responses_api_bridge.transformation_handler.get_model_response_iterator( 

605 streaming_response=cast(AsyncIterator[str], response), 

606 sync_stream=False, 

607 json_mode=False, 

608 ) 

609 # Wrap in CustomStreamWrapper to get the async generator 

610 logging_obj: Final = request_data.get("litellm_logging_obj") 

611 streamwrapper: Final = CustomStreamWrapper( 

612 completion_stream=completion_stream, 

613 model=request_data.get("model", ""), 

614 custom_llm_provider=None, 

615 logging_obj=logging_obj, 

616 ) 

617 # Use async_data_generator to format as SSE 

618 return async_data_generator( 

619 response=streamwrapper, 

620 user_api_key_dict=user_api_key_dict, 

621 request_data=request_data, 

622 request=request, 

623 ) 

624 # Otherwise, use the default generator 

625 return async_data_generator( 

626 response=response, 

627 user_api_key_dict=user_api_key_dict, 

628 request_data=request_data, 

629 request=request, 

630 ) 

631 

632 try: 

633 response: Final = await processor.base_process_llm_request( 

634 request=request, 

635 fastapi_response=fastapi_response, 

636 user_api_key_dict=user_api_key_dict, 

637 route_type="aresponses", 

638 proxy_logging_obj=proxy_logging_obj, 

639 llm_router=llm_router, 

640 general_settings=general_settings, 

641 proxy_config=proxy_config, 

642 select_data_generator=cursor_data_generator, 

643 model=None, 

644 user_model=user_model, 

645 user_temperature=user_temperature, 

646 user_request_timeout=user_request_timeout, 

647 user_max_tokens=user_max_tokens, 

648 user_api_base=user_api_base, 

649 version=version, 

650 ) 

651 

652 # Transform non-streaming Responses API response to chat completions format 

653 if isinstance(response, ResponsesAPIResponse): 

654 logging_obj: Final = processor.data.get("litellm_logging_obj") 

655 transformed_response: Final = responses_api_bridge.transformation_handler.transform_response( 

656 model=processor.data.get("model", ""), 

657 raw_response=response, 

658 model_response=ModelResponse(), 

659 logging_obj=cast(Any, logging_obj), 

660 request_data=processor.data, 

661 messages=processor.data.get("input", []), 

662 optional_params={}, 

663 litellm_params={}, 

664 encoding=None, 

665 api_key=None, 

666 json_mode=None, 

667 ) 

668 return transformed_response 

669 

670 # Streaming responses are already transformed by cursor_select_data_generator 

671 return response 

672 except Exception as e: 

673 raise await processor._handle_llm_api_exception( 

674 e=e, 

675 user_api_key_dict=user_api_key_dict, 

676 proxy_logging_obj=proxy_logging_obj, 

677 version=version, 

678 ) 

679 

680 

681@router.get( 

682 "/v1/responses/{response_id}", 

683 dependencies=[Depends(user_api_key_auth)], 

684 tags=["responses"], 

685 responses=RESPONSES_API_RESPONSE_SCHEMAS, 

686) 

687@router.get( 

688 "/responses/{response_id}", 

689 dependencies=[Depends(user_api_key_auth)], 

690 tags=["responses"], 

691 responses=RESPONSES_API_RESPONSE_SCHEMAS, 

692) 

693@router.get( 

694 "/openai/v1/responses/{response_id}", 

695 dependencies=[Depends(user_api_key_auth)], 

696 tags=["responses"], 

697 responses=RESPONSES_API_RESPONSE_SCHEMAS, 

698) 

699async def get_response( 

700 response_id: str, 

701 request: Request, 

702 fastapi_response: Response, 

703 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

704): 

705 """ 

706 Get a response by ID. 

707  

708 Supports both: 

709 - Polling IDs (litellm_poll_*): Returns cumulative cached content from background responses 

710 - Provider response IDs: Passes through to provider API 

711  

712 Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses/get 

713  

714 ```bash 

715 # Get polling response 

716 curl -X GET http://localhost:4000/v1/responses/litellm_poll_abc123 \ 

717 -H "Authorization: Bearer sk-1234" 

718  

719 # Get provider response 

720 curl -X GET http://localhost:4000/v1/responses/resp_abc123 \ 

721 -H "Authorization: Bearer sk-1234" 

722 ``` 

723 """ 

724 from litellm.proxy.proxy_server import ( 

725 _read_request_body, 

726 general_settings, 

727 llm_router, 

728 proxy_config, 

729 proxy_logging_obj, 

730 redis_usage_cache, 

731 select_data_generator, 

732 user_api_base, 

733 user_max_tokens, 

734 user_model, 

735 user_request_timeout, 

736 user_temperature, 

737 version, 

738 ) 

739 from litellm.proxy.response_polling.polling_handler import ResponsePollingHandler 

740 

741 # Check if this is a polling ID 

742 if ResponsePollingHandler.is_polling_id(response_id): 742 ↛ 744line 742 didn't jump to line 744 because the condition on line 742 was never true

743 # Handle polling response 

744 if not redis_usage_cache: 

745 raise HTTPException( 

746 status_code=500, 

747 detail="Redis cache not configured. Polling requires Redis.", 

748 ) 

749 

750 polling_handler: Final = ResponsePollingHandler(redis_cache=redis_usage_cache) 

751 

752 # Get current state from cache 

753 state: Final = await polling_handler.get_state(response_id) 

754 

755 if not state: 

756 raise HTTPException( 

757 status_code=404, 

758 detail=f"Polling response {response_id} not found or expired", 

759 ) 

760 

761 # Return the whole state directly (OpenAI Response object format) 

762 # https://platform.openai.com/docs/api-reference/responses/object 

763 return state 

764 

765 # Normal provider response flow 

766 data: Final = await _read_request_body(request=request) 

767 data["response_id"] = response_id 

768 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

769 try: 

770 return await processor.base_process_llm_request( 

771 request=request, 

772 fastapi_response=fastapi_response, 

773 user_api_key_dict=user_api_key_dict, 

774 route_type="aget_responses", 

775 proxy_logging_obj=proxy_logging_obj, 

776 llm_router=llm_router, 

777 general_settings=general_settings, 

778 proxy_config=proxy_config, 

779 select_data_generator=select_data_generator, 

780 model=None, 

781 user_model=user_model, 

782 user_temperature=user_temperature, 

783 user_request_timeout=user_request_timeout, 

784 user_max_tokens=user_max_tokens, 

785 user_api_base=user_api_base, 

786 version=version, 

787 ) 

788 except Exception as e: 

789 raise await processor._handle_llm_api_exception( 

790 e=e, 

791 user_api_key_dict=user_api_key_dict, 

792 proxy_logging_obj=proxy_logging_obj, 

793 version=version, 

794 ) 

795 

796 

797@router.delete( 

798 "/v1/responses/{response_id}", 

799 dependencies=[Depends(user_api_key_auth)], 

800 tags=["responses"], 

801 responses=DELETE_RESPONSE_SCHEMAS, 

802) 

803@router.delete( 

804 "/responses/{response_id}", 

805 dependencies=[Depends(user_api_key_auth)], 

806 tags=["responses"], 

807 responses=DELETE_RESPONSE_SCHEMAS, 

808) 

809@router.delete( 

810 "/openai/v1/responses/{response_id}", 

811 dependencies=[Depends(user_api_key_auth)], 

812 tags=["responses"], 

813 responses=DELETE_RESPONSE_SCHEMAS, 

814) 

815async def delete_response( 

816 response_id: str, 

817 request: Request, 

818 fastapi_response: Response, 

819 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

820): 

821 """ 

822 Delete a response by ID. 

823  

824 Supports both: 

825 - Polling IDs (litellm_poll_*): Deletes from Redis cache 

826 - Provider response IDs: Passes through to provider API 

827  

828 Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses/delete 

829  

830 ```bash 

831 curl -X DELETE http://localhost:4000/v1/responses/resp_abc123 \ 

832 -H "Authorization: Bearer sk-1234" 

833 ``` 

834 """ 

835 from litellm.proxy.proxy_server import ( 

836 _read_request_body, 

837 general_settings, 

838 llm_router, 

839 proxy_config, 

840 proxy_logging_obj, 

841 redis_usage_cache, 

842 select_data_generator, 

843 user_api_base, 

844 user_max_tokens, 

845 user_model, 

846 user_request_timeout, 

847 user_temperature, 

848 version, 

849 ) 

850 from litellm.proxy.response_polling.polling_handler import ResponsePollingHandler 

851 

852 # Check if this is a polling ID 

853 if ResponsePollingHandler.is_polling_id(response_id): 853 ↛ 855line 853 didn't jump to line 855 because the condition on line 853 was never true

854 # Handle polling response deletion 

855 if not redis_usage_cache: 

856 raise HTTPException(status_code=500, detail="Redis cache not configured.") 

857 

858 polling_handler: Final = ResponsePollingHandler(redis_cache=redis_usage_cache) 

859 

860 # Get state to verify access 

861 state: Final = await polling_handler.get_state(response_id) 

862 

863 if not state: 

864 raise HTTPException(status_code=404, detail=f"Polling response {response_id} not found") 

865 

866 # Delete from cache 

867 success: Final = await polling_handler.delete_polling(response_id) 

868 

869 if success: 

870 return DeleteResponseResult(id=response_id, object="response", deleted=True) 

871 else: 

872 raise HTTPException(status_code=500, detail="Failed to delete polling response") 

873 

874 # Normal provider response flow 

875 data: Final = await _read_request_body(request=request) 

876 data["response_id"] = response_id 

877 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

878 try: 

879 return await processor.base_process_llm_request( 

880 request=request, 

881 fastapi_response=fastapi_response, 

882 user_api_key_dict=user_api_key_dict, 

883 route_type="adelete_responses", 

884 proxy_logging_obj=proxy_logging_obj, 

885 llm_router=llm_router, 

886 general_settings=general_settings, 

887 proxy_config=proxy_config, 

888 select_data_generator=select_data_generator, 

889 model=None, 

890 user_model=user_model, 

891 user_temperature=user_temperature, 

892 user_request_timeout=user_request_timeout, 

893 user_max_tokens=user_max_tokens, 

894 user_api_base=user_api_base, 

895 version=version, 

896 ) 

897 except Exception as e: 

898 raise await processor._handle_llm_api_exception( 

899 e=e, 

900 user_api_key_dict=user_api_key_dict, 

901 proxy_logging_obj=proxy_logging_obj, 

902 version=version, 

903 ) 

904 

905 

906@router.get( 

907 "/v1/responses/{response_id}/input_items", 

908 dependencies=[Depends(user_api_key_auth)], 

909 tags=["responses"], 

910 responses=RESPONSE_ITEM_LIST_SCHEMAS, 

911) 

912@router.get( 

913 "/responses/{response_id}/input_items", 

914 dependencies=[Depends(user_api_key_auth)], 

915 tags=["responses"], 

916 responses=RESPONSE_ITEM_LIST_SCHEMAS, 

917) 

918@router.get( 

919 "/openai/v1/responses/{response_id}/input_items", 

920 dependencies=[Depends(user_api_key_auth)], 

921 tags=["responses"], 

922 responses=RESPONSE_ITEM_LIST_SCHEMAS, 

923) 

924async def get_response_input_items( 

925 response_id: str, 

926 request: Request, 

927 fastapi_response: Response, 

928 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

929): 

930 """List input items for a response.""" 

931 from litellm.proxy.proxy_server import ( 

932 _read_request_body, 

933 general_settings, 

934 llm_router, 

935 proxy_config, 

936 proxy_logging_obj, 

937 select_data_generator, 

938 user_api_base, 

939 user_max_tokens, 

940 user_model, 

941 user_request_timeout, 

942 user_temperature, 

943 version, 

944 ) 

945 

946 data: Final = await _read_request_body(request=request) 

947 data["response_id"] = response_id 

948 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

949 try: 

950 return await processor.base_process_llm_request( 

951 request=request, 

952 fastapi_response=fastapi_response, 

953 user_api_key_dict=user_api_key_dict, 

954 route_type="alist_input_items", 

955 proxy_logging_obj=proxy_logging_obj, 

956 llm_router=llm_router, 

957 general_settings=general_settings, 

958 proxy_config=proxy_config, 

959 select_data_generator=select_data_generator, 

960 model=None, 

961 user_model=user_model, 

962 user_temperature=user_temperature, 

963 user_request_timeout=user_request_timeout, 

964 user_max_tokens=user_max_tokens, 

965 user_api_base=user_api_base, 

966 version=version, 

967 ) 

968 except Exception as e: 

969 raise await processor._handle_llm_api_exception( 

970 e=e, 

971 user_api_key_dict=user_api_key_dict, 

972 proxy_logging_obj=proxy_logging_obj, 

973 version=version, 

974 ) 

975 

976 

977@router.post( 

978 "/v1/responses/compact", 

979 dependencies=[Depends(user_api_key_auth)], 

980 tags=["responses"], 

981) 

982@router.post( 

983 "/responses/compact", 

984 dependencies=[Depends(user_api_key_auth)], 

985 tags=["responses"], 

986) 

987@router.post( 

988 "/openai/v1/responses/compact", 

989 dependencies=[Depends(user_api_key_auth)], 

990 tags=["responses"], 

991) 

992async def compact_response( 

993 request: Request, 

994 fastapi_response: Response, 

995 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

996): 

997 """ 

998 Compact a response by running a compaction pass over a conversation. 

999  

1000 Returns encrypted, opaque items that can be used to reduce context size. 

1001  

1002 Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses/compact 

1003  

1004 ```bash 

1005 curl -X POST http://localhost:4000/v1/responses/compact \ 

1006 -H "Content-Type: application/json" \ 

1007 -H "Authorization: Bearer sk-1234" \ 

1008 -d '{ 

1009 "model": "gpt-4o", 

1010 "input": [{"role": "user", "content": "Hello"}] 

1011 }' 

1012 ``` 

1013 """ 

1014 from litellm.proxy.proxy_server import ( 

1015 _read_request_body, 

1016 general_settings, 

1017 llm_router, 

1018 proxy_config, 

1019 proxy_logging_obj, 

1020 select_data_generator, 

1021 user_api_base, 

1022 user_max_tokens, 

1023 user_model, 

1024 user_request_timeout, 

1025 user_temperature, 

1026 version, 

1027 ) 

1028 

1029 data: Final = await _read_request_body(request=request) 

1030 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

1031 try: 

1032 return await processor.base_process_llm_request( 

1033 request=request, 

1034 fastapi_response=fastapi_response, 

1035 user_api_key_dict=user_api_key_dict, 

1036 route_type="acompact_responses", 

1037 proxy_logging_obj=proxy_logging_obj, 

1038 llm_router=llm_router, 

1039 general_settings=general_settings, 

1040 proxy_config=proxy_config, 

1041 select_data_generator=select_data_generator, 

1042 model=None, 

1043 user_model=user_model, 

1044 user_temperature=user_temperature, 

1045 user_request_timeout=user_request_timeout, 

1046 user_max_tokens=user_max_tokens, 

1047 user_api_base=user_api_base, 

1048 version=version, 

1049 ) 

1050 except Exception as e: 

1051 raise await processor._handle_llm_api_exception( 

1052 e=e, 

1053 user_api_key_dict=user_api_key_dict, 

1054 proxy_logging_obj=proxy_logging_obj, 

1055 version=version, 

1056 ) 

1057 

1058 

1059class _ResponsesApiErrorDetail(TypedDict): 

1060 message: ReadOnly[str] 

1061 type: ReadOnly[str] 

1062 param: ReadOnly[str | None] 

1063 code: ReadOnly[str | None] 

1064 

1065 

1066class _ResponsesApiErrorBody(TypedDict): 

1067 error: ReadOnly[_ResponsesApiErrorDetail] 

1068 

1069 

1070class _ResponsesInputTokensResult(TypedDict): 

1071 object: ReadOnly[str] 

1072 input_tokens: ReadOnly[int] 

1073 

1074 

1075class _TokenCountPayload(TypedDict): 

1076 model: ReadOnly[str] 

1077 messages: ReadOnly[tuple[Mapping[str, object], ...]] 

1078 tools: ReadOnly[object] 

1079 

1080 

1081class _TokenCounter(Protocol): 

1082 def __call__(self, request: TokenCountRequest, call_endpoint: bool) -> Awaitable[TokenCountResponse]: ... 1082 ↛ exitline 1082 didn't return from function '__call__' because

1083 

1084 

1085def _proxy_token_counter() -> _TokenCounter: 

1086 from litellm.proxy.proxy_server import token_counter 

1087 

1088 return token_counter 

1089 

1090 

1091_token_counter_dep: Final = Depends(_proxy_token_counter) 

1092 

1093 

1094def _responses_invalid_request_response(message: str, param: str | None, code: str | None) -> JSONResponse: 

1095 body: Final[_ResponsesApiErrorBody] = { 

1096 "error": { 

1097 "message": message, 

1098 "type": "invalid_request_error", 

1099 "param": param, 

1100 "code": code, 

1101 } 

1102 } 

1103 return JSONResponse(status_code=400, content=body) 

1104 

1105 

1106def _missing_responses_param_response(param: str) -> JSONResponse: 

1107 return _responses_invalid_request_response( 

1108 message=f"Missing required parameter: '{param}'.", 

1109 param=param, 

1110 code="missing_required_parameter", 

1111 ) 

1112 

1113 

1114def _responses_input_as_token_count_messages( 

1115 input_value: str | ResponseInputParam, 

1116 instructions: str | None, 

1117) -> tuple[Mapping[str, object], ...]: 

1118 from litellm.responses.litellm_completion_transformation.transformation import ( 

1119 LiteLLMCompletionResponsesConfig, 

1120 ) 

1121 

1122 request_params: Final[ResponsesAPIOptionalRequestParams] = {"instructions": instructions} 

1123 transformed: Final = LiteLLMCompletionResponsesConfig.transform_responses_api_input_to_messages( 

1124 input=input_value, 

1125 responses_api_request=request_params, 

1126 ) 

1127 return tuple( 

1128 message if isinstance(message, dict) else message.model_dump(exclude_none=True) for message in transformed 

1129 ) 

1130 

1131 

1132@router.post( 

1133 "/v1/responses/input_tokens", 

1134 dependencies=(_user_api_key_auth_dep,), 

1135 tags=_RESPONSES_TAGS, 

1136) 

1137@router.post( 

1138 "/responses/input_tokens", 

1139 dependencies=(_user_api_key_auth_dep,), 

1140 tags=_RESPONSES_TAGS, 

1141) 

1142@router.post( 

1143 "/openai/v1/responses/input_tokens", 

1144 dependencies=(_user_api_key_auth_dep,), 

1145 tags=_RESPONSES_TAGS, 

1146) 

1147async def responses_input_tokens( 

1148 request: Request, 

1149 token_counter: _TokenCounter = _token_counter_dep, 

1150): 

1151 """ 

1152 Count the input tokens of a Responses API request without calling the model. 

1153 

1154 Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses/input-tokens 

1155 

1156 ```bash 

1157 curl -X POST http://localhost:4000/v1/responses/input_tokens \ 

1158 -H "Content-Type: application/json" \ 

1159 -H "Authorization: Bearer sk-1234" \ 

1160 -d '{ 

1161 "model": "gpt-4o", 

1162 "input": "Hello, how are you?" 

1163 }' 

1164 ``` 

1165 

1166 Returns: `{"object": "response.input_tokens", "input_tokens": <count>}` 

1167 """ 

1168 data: Final = await _read_request_body(request=request) 

1169 model_name: Final = data.get("model") 

1170 input_value: Final = data.get("input") 

1171 if not isinstance(model_name, str) or not model_name: 1171 ↛ 1173line 1171 didn't jump to line 1173 because the condition on line 1171 was always true

1172 return _missing_responses_param_response("model") 

1173 if input_value is None: 

1174 return _missing_responses_param_response("input") 

1175 if isinstance(input_value, (str, list)) and not input_value: 

1176 return _responses_invalid_request_response( 

1177 message="""One of "input" or "previous_response_id" or 'prompt' or 'conversation' must be provided.""", 

1178 param=None, 

1179 code="missing_required_parameter", 

1180 ) 

1181 

1182 try: 

1183 payload: Final[_TokenCountPayload] = { 

1184 "model": model_name, 

1185 "messages": _responses_input_as_token_count_messages( 

1186 input_value=input_value, 

1187 instructions=data.get("instructions"), 

1188 ), 

1189 "tools": data.get("tools"), 

1190 } 

1191 token_request: Final = TokenCountRequest.model_validate(payload) 

1192 except Exception as e: 

1193 return _responses_invalid_request_response( 

1194 message=f"Invalid request for token counting: {e}", param=None, code=None 

1195 ) 

1196 

1197 token_response: Final = await token_counter(request=token_request, call_endpoint=True) 

1198 result: Final[_ResponsesInputTokensResult] = { 

1199 "object": "response.input_tokens", 

1200 "input_tokens": token_response.total_tokens, 

1201 } 

1202 return result 

1203 

1204 

1205@router.post( 

1206 "/v1/responses/{response_id}/cancel", 

1207 dependencies=[Depends(user_api_key_auth)], 

1208 tags=["responses"], 

1209) 

1210@router.post( 

1211 "/responses/{response_id}/cancel", 

1212 dependencies=[Depends(user_api_key_auth)], 

1213 tags=["responses"], 

1214) 

1215@router.post( 

1216 "/openai/v1/responses/{response_id}/cancel", 

1217 dependencies=[Depends(user_api_key_auth)], 

1218 tags=["responses"], 

1219) 

1220async def cancel_response( 

1221 response_id: str, 

1222 request: Request, 

1223 fastapi_response: Response, 

1224 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

1225): 

1226 """ 

1227 Cancel a response by ID. 

1228  

1229 Supports both: 

1230 - Polling IDs (litellm_poll_*): Cancels background response and updates status in Redis 

1231 - Provider response IDs: Passes through to provider API 

1232  

1233 Follows the OpenAI Responses API spec: https://platform.openai.com/docs/api-reference/responses/cancel 

1234  

1235 ```bash 

1236 # Cancel polling response 

1237 curl -X POST http://localhost:4000/v1/responses/litellm_poll_abc123/cancel \ 

1238 -H "Authorization: Bearer sk-1234" 

1239  

1240 # Cancel provider response 

1241 curl -X POST http://localhost:4000/v1/responses/resp_abc123/cancel \ 

1242 -H "Authorization: Bearer sk-1234" 

1243 ``` 

1244 """ 

1245 from litellm.proxy.proxy_server import ( 

1246 _read_request_body, 

1247 general_settings, 

1248 llm_router, 

1249 proxy_config, 

1250 proxy_logging_obj, 

1251 redis_usage_cache, 

1252 select_data_generator, 

1253 user_api_base, 

1254 user_max_tokens, 

1255 user_model, 

1256 user_request_timeout, 

1257 user_temperature, 

1258 version, 

1259 ) 

1260 from litellm.proxy.response_polling.polling_handler import ResponsePollingHandler 

1261 

1262 # Check if this is a polling ID 

1263 if ResponsePollingHandler.is_polling_id(response_id): 1263 ↛ 1265line 1263 didn't jump to line 1265 because the condition on line 1263 was never true

1264 # Handle polling response cancellation 

1265 if not redis_usage_cache: 

1266 raise HTTPException(status_code=500, detail="Redis cache not configured.") 

1267 

1268 polling_handler: Final = ResponsePollingHandler(redis_cache=redis_usage_cache) 

1269 

1270 # Get current state to verify it exists 

1271 state: Final = await polling_handler.get_state(response_id) 

1272 

1273 if not state: 

1274 raise HTTPException(status_code=404, detail=f"Polling response {response_id} not found") 

1275 

1276 # Cancel the polling response (sets status to "cancelled") 

1277 success: Final = await polling_handler.cancel_polling(response_id) 

1278 

1279 if success: 

1280 # Fetch the updated state with cancelled status 

1281 updated_state: Final = await polling_handler.get_state(response_id) 

1282 

1283 # Return the whole state directly (now with status="cancelled") 

1284 return updated_state 

1285 else: 

1286 raise HTTPException(status_code=500, detail="Failed to cancel polling response") 

1287 

1288 # Normal provider response flow 

1289 data: Final = await _read_request_body(request=request) 

1290 data["response_id"] = response_id 

1291 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

1292 try: 

1293 return await processor.base_process_llm_request( 

1294 request=request, 

1295 fastapi_response=fastapi_response, 

1296 user_api_key_dict=user_api_key_dict, 

1297 route_type="acancel_responses", 

1298 proxy_logging_obj=proxy_logging_obj, 

1299 llm_router=llm_router, 

1300 general_settings=general_settings, 

1301 proxy_config=proxy_config, 

1302 select_data_generator=select_data_generator, 

1303 model=None, 

1304 user_model=user_model, 

1305 user_temperature=user_temperature, 

1306 user_request_timeout=user_request_timeout, 

1307 user_max_tokens=user_max_tokens, 

1308 user_api_base=user_api_base, 

1309 version=version, 

1310 ) 

1311 except Exception as e: 

1312 raise await processor._handle_llm_api_exception( 

1313 e=e, 

1314 user_api_key_dict=user_api_key_dict, 

1315 proxy_logging_obj=proxy_logging_obj, 

1316 version=version, 

1317 ) 

1318 

1319 

1320async def _read_ws_model_from_first_frame( 

1321 websocket: WebSocket, 

1322 query_model: str | None = None, 

1323) -> tuple[str, str] | None: 

1324 """Read the first WS frame and return (model, raw_message), or None on error. 

1325 

1326 Sends an appropriate error frame and closes the socket before returning None. 

1327 """ 

1328 try: 

1329 first_message: Final = await asyncio.wait_for(websocket.receive_text(), timeout=30) 

1330 except asyncio.TimeoutError: 

1331 await websocket.close(code=1008, reason="Timed out waiting for first message") 

1332 return None 

1333 except WebSocketDisconnect: 

1334 return None 

1335 except Exception: 

1336 verbose_proxy_logger.exception("Responses WebSocket error reading first message") 

1337 await websocket.close(code=1011, reason="Internal server error") 

1338 return None 

1339 

1340 try: 

1341 first_event: Final = json.loads(first_message) 

1342 except json.JSONDecodeError: 

1343 await websocket.send_text( 

1344 json.dumps( 

1345 { 

1346 "type": "error", 

1347 "error": { 

1348 "type": "invalid_request_error", 

1349 "message": "First message is not valid JSON.", 

1350 }, 

1351 } 

1352 ) 

1353 ) 

1354 await websocket.close(code=1008, reason="Invalid JSON in first message") 

1355 return None 

1356 

1357 if not isinstance(first_event, dict) or first_event.get("type") != "response.create": 

1358 await websocket.send_text( 

1359 json.dumps( 

1360 { 

1361 "type": "error", 

1362 "error": { 

1363 "type": "invalid_request_error", 

1364 "message": "First message must be a response.create JSON object.", 

1365 }, 

1366 } 

1367 ) 

1368 ) 

1369 await websocket.close(code=1008, reason="Invalid first message") 

1370 return None 

1371 

1372 model: Final = query_model or _extract_model_from_first_ws_event(first_event) 

1373 if not model: 

1374 await websocket.send_text( 

1375 json.dumps( 

1376 { 

1377 "type": "error", 

1378 "error": { 

1379 "type": "invalid_request_error", 

1380 "message": "No model provided. Supply ?model=<model> in the URL or include 'model' in the first response.create event.", 

1381 }, 

1382 } 

1383 ) 

1384 ) 

1385 await websocket.close(code=1008, reason="No model provided") 

1386 return None 

1387 

1388 return model, first_message 

1389 

1390 

1391def _extract_model_from_first_ws_event(first_event: object) -> str | None: 

1392 """Extract model from a response.create WS event, handling flat and nested formats. 

1393 

1394 Flat: {"type": "response.create", "model": "gpt-4o", ...} 

1395 Nested: {"type": "response.create", "response": {"model": "gpt-4o", ...}} 

1396 """ 

1397 if not isinstance(first_event, dict): 

1398 return None 

1399 nested: Final = first_event.get("response") 

1400 return (nested.get("model") if isinstance(nested, dict) else None) or first_event.get("model") 

1401 

1402 

1403class _ResponseCreateRoutingHints(BaseModel): 

1404 model_config = ConfigDict(extra="ignore", frozen=True) 

1405 

1406 input: str | Sequence[object] | None = None 

1407 previous_response_id: str | None = None 

1408 response: "_ResponseCreateRoutingHints | None" = None 

1409 

1410 

1411def _routing_hints_from_first_ws_frame(first_message: str) -> Mapping[str, object]: 

1412 try: 

1413 frame: Final = _ResponseCreateRoutingHints.model_validate_json(first_message) 

1414 except ValidationError: 

1415 return EMPTY_MAPPING 

1416 nested: Final = frame.response or frame 

1417 hints: Final = { 

1418 "input": frame.input if nested.input is None else nested.input, 

1419 "previous_response_id": ( 

1420 frame.previous_response_id if nested.previous_response_id is None else nested.previous_response_id 

1421 ), 

1422 } 

1423 return MappingProxyType({key: value for key, value in hints.items() if value is not None}) 

1424 

1425 

1426def _responses_ws_failure_frame(failure: Exception) -> str: 

1427 raw_status: Final = getattr(failure, "status_code", None) 

1428 status: Final = raw_status if isinstance(raw_status, int) and not isinstance(raw_status, bool) else 500 

1429 error_type: Final = ( 

1430 "rate_limit_exceeded" if status == 429 else "invalid_request_error" if 400 <= status < 500 else "server_error" 

1431 ) 

1432 return json.dumps({"type": "error", "status": status, "error": {"type": error_type, "message": str(failure)}}) 

1433 

1434 

1435async def _enforce_responses_ws_first_frame_model_auth( 

1436 request: Request, 

1437 model: str, 

1438 user_api_key_dict: UserAPIKeyAuth, 

1439 llm_router: "Router | None", 

1440) -> None: 

1441 from litellm.proxy.auth.user_api_key_auth import ( 

1442 _enforce_key_and_fallback_model_access, 

1443 _run_centralized_common_checks, 

1444 ) 

1445 from litellm.proxy.proxy_server import ( 

1446 general_settings, 

1447 llm_model_list, 

1448 master_key, 

1449 user_custom_auth, 

1450 ) 

1451 

1452 request_data: Final = {"model": model} 

1453 route: Final = request.scope.get("path") or "/v1/responses" 

1454 if master_key is None and not ( 

1455 general_settings.get("enable_jwt_auth", False) 

1456 or general_settings.get("enable_oauth2_auth", False) 

1457 or general_settings.get("enable_oauth2_proxy_auth", False) 

1458 ): 

1459 return 

1460 if user_custom_auth is not None and not general_settings.get("custom_auth_run_common_checks", False): 

1461 return 

1462 await _enforce_key_and_fallback_model_access( 

1463 valid_token=user_api_key_dict, 

1464 request_data=request_data, 

1465 route=route, 

1466 request=request, 

1467 llm_model_list=llm_model_list, 

1468 llm_router=llm_router, 

1469 ) 

1470 await _run_centralized_common_checks( 

1471 user_api_key_auth_obj=user_api_key_dict, 

1472 request=request, 

1473 request_data=request_data, 

1474 route=route, 

1475 ) 

1476 

1477 

1478@router.websocket("/v1/responses") 

1479@router.websocket("/responses") 

1480async def responses_websocket_endpoint( 

1481 websocket: WebSocket, 

1482 model: str | None = fastapi.Query(None, description="The model to use for the responses WebSocket session."), 

1483 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth_websocket), 

1484): 

1485 """ 

1486 Responses API WebSocket mode endpoint. 

1487 

1488 Keeps a persistent WebSocket connection for response.create events, 

1489 enabling lower-latency agentic workflows with many tool-call round trips. 

1490 

1491 Follows the OpenAI split: the bearer token is validated at connection time 

1492 (before accept); the model is resolved either from the ?model= query param 

1493 or from the first response.create frame, whichever is present. 

1494 

1495 See: https://developers.openai.com/api/docs/guides/websocket-mode/ 

1496 """ 

1497 from litellm.proxy.proxy_server import ( 

1498 general_settings, 

1499 llm_router, 

1500 proxy_config, 

1501 proxy_logging_obj, 

1502 user_api_base, 

1503 user_max_tokens, 

1504 user_model, 

1505 user_request_timeout, 

1506 user_temperature, 

1507 version, 

1508 ) 

1509 from litellm.proxy.route_llm_request import route_request 

1510 

1511 # Accept the WebSocket handshake. Key was already validated by the Depends 

1512 # above; we can safely accept regardless of whether ?model= was supplied. 

1513 requested_protocols: Final = [ 

1514 p.strip() for p in (websocket.headers.get("sec-websocket-protocol") or "").split(",") if p.strip() 

1515 ] 

1516 accept_kwargs: Final[dict] = {} 

1517 if requested_protocols: 

1518 accept_kwargs["subprotocol"] = requested_protocols[0] 

1519 await websocket.accept(**accept_kwargs) 

1520 

1521 result: Final = await _read_ws_model_from_first_frame(websocket, query_model=model) 

1522 if result is None: 

1523 return 

1524 resolved_model, first_message = result 

1525 

1526 data: dict[str, object] = { 

1527 "model": resolved_model, 

1528 "websocket": websocket, 

1529 "first_message": first_message, 

1530 } 

1531 

1532 # Construct a synthetic Request for pre-call processing 

1533 headers_list: Final = list(websocket.scope.get("headers") or []) 

1534 scope: Final[dict[str, object]] = { 

1535 "type": "http", 

1536 "method": "POST", 

1537 "path": "/v1/responses", 

1538 "headers": headers_list, 

1539 } 

1540 request: Final = Request(scope=scope) 

1541 request._url = websocket.url 

1542 

1543 _body_bytes: Final = json.dumps({"model": resolved_model}).encode() 

1544 

1545 async def return_body(): 

1546 return _body_bytes 

1547 

1548 request.body = return_body 

1549 

1550 # Phase 1: pre-call processing (auth, guardrails, rate limits) 

1551 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

1552 try: 

1553 if not model: 

1554 await _enforce_responses_ws_first_frame_model_auth( 

1555 request=request, 

1556 model=resolved_model, 

1557 user_api_key_dict=user_api_key_dict, 

1558 llm_router=llm_router, 

1559 ) 

1560 ( 

1561 data, 

1562 litellm_logging_obj, 

1563 ) = await base_llm_response_processor.common_processing_pre_call_logic( 

1564 request=request, 

1565 general_settings=general_settings, 

1566 user_api_key_dict=user_api_key_dict, 

1567 version=version, 

1568 proxy_logging_obj=proxy_logging_obj, 

1569 proxy_config=proxy_config, 

1570 user_model=user_model, 

1571 user_temperature=user_temperature, 

1572 user_request_timeout=user_request_timeout, 

1573 user_max_tokens=user_max_tokens, 

1574 user_api_base=user_api_base, 

1575 model=resolved_model, 

1576 route_type="_aresponses_websocket", 

1577 ) 

1578 except Exception as e: 

1579 verbose_proxy_logger.exception("Responses WebSocket pre-call error") 

1580 try: 

1581 await websocket.send_text( 

1582 json.dumps( 

1583 { 

1584 "type": "error", 

1585 "error": { 

1586 "type": "invalid_request_error", 

1587 "message": str(e), 

1588 }, 

1589 } 

1590 ) 

1591 ) 

1592 except Exception: 

1593 pass 

1594 await websocket.close(code=1008, reason="Pre-call error") 

1595 return 

1596 

1597 routed_data: Final = dict( 

1598 data, user_api_key_dict=user_api_key_dict, **_routing_hints_from_first_ws_frame(first_message) 

1599 ) 

1600 # Phase 2: route to upstream provider 

1601 try: 

1602 llm_call: Final = await route_request( 

1603 data=routed_data, 

1604 route_type="_aresponses_websocket", 

1605 llm_router=llm_router, 

1606 user_model=user_model, 

1607 ) 

1608 failure: Final = await llm_call 

1609 if isinstance(failure, Exception): 

1610 await proxy_logging_obj.post_call_failure_hook( 

1611 user_api_key_dict=user_api_key_dict, 

1612 original_exception=failure, 

1613 request_data=routed_data, 

1614 ) 

1615 except Exception as e: 

1616 verbose_proxy_logger.exception("Responses WebSocket error") 

1617 with contextlib.suppress(Exception): 

1618 await websocket.send_text(_responses_ws_failure_frame(e)) 

1619 await proxy_logging_obj.post_call_failure_hook( 

1620 user_api_key_dict=user_api_key_dict, 

1621 original_exception=e, 

1622 request_data=routed_data, 

1623 ) 

1624 await websocket.close(code=1011, reason="Internal server error")