"""Public stateful OpenAI Responses-compatible text endpoints.""" from __future__ import annotations import asyncio import json from collections.abc import AsyncIterator from typing import Any import settings from db.session import SessionLocal from fastapi import APIRouter, Request from fastapi.responses import JSONResponse, StreamingResponse from loguru import logger from services.openai_realtime.auth import ( RealtimeAuthError, RealtimeCredential, authenticate_bearer, bearer_from_authorization, ) from services.openai_responses.protocol import ( ResponseRequest, ResponsesAPIError, error_payload, new_id, parse_request, response_payload, ) from services.openai_responses.runner import ResponseRunError, ResponsesTextRunner from services.openai_responses.state import ( PreparedResponse, finish_response, get_response, prepare_response, ) router = APIRouter(prefix="/v1/responses", tags=["openai-responses"]) async def _credential(request: Request) -> RealtimeCredential: token = bearer_from_authorization(request.headers.get("authorization")) async with SessionLocal() as db: credential = await authenticate_bearer(db, token) if credential.ephemeral: raise RealtimeAuthError("A long-lived API key is required") return credential def _json_error(error: ResponsesAPIError) -> JSONResponse: return JSONResponse(status_code=error.status_code, content=error_payload(error)) def _sse(event: dict[str, Any]) -> bytes: return ( f"event: {event['type']}\n" f"data: {json.dumps(event, ensure_ascii=False)}\n\n" ).encode("utf-8") def _text_item(item_id: str, text: str) -> dict[str, Any]: return { "id": item_id, "type": "message", "status": "completed", "role": "assistant", "content": [ { "type": "output_text", "text": text, "annotations": [], } ], } def _output_items( *, text: str, text_item_id: str, function_calls: list[dict[str, Any]], ) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] if text: items.append(_text_item(text_item_id, text)) items.extend(function_calls) return items async def _run_prepared( prepared: PreparedResponse, *, on_delta=None, text_item_id: str, ): try: async with asyncio.timeout(settings.RESPONSES_REQUEST_TIMEOUT_SECONDS): result = await ResponsesTextRunner().run( prepared.config, history_messages=prepared.history_messages, current_messages=prepared.current_messages, runtime_state=prepared.runtime_state, client_tool_results=prepared.client_tool_results, on_delta=on_delta, ) return await finish_response( prepared.row.id, status="completed", output_items=_output_items( text=result.text, text_item_id=text_item_id, function_calls=result.function_calls, ), context_messages=result.context_messages, dynamic_variables=result.dynamic_variables, runtime_state=result.runtime_state, ) except TimeoutError as exc: await finish_response( prepared.row.id, status="failed", output_items=[], error={"code": "response_timeout", "message": "Response timed out"}, ) raise ResponsesAPIError( "Response timed out", code="response_timeout", status_code=504 ) from exc except asyncio.CancelledError: await finish_response( prepared.row.id, status="cancelled", output_items=[], error={"code": "client_disconnected", "message": "Client disconnected"}, ) raise except ResponseRunError as exc: await finish_response( prepared.row.id, status="failed", output_items=[], error={"code": "response_failed", "message": str(exc)}, ) raise ResponsesAPIError( "Response generation failed", code="response_failed", status_code=500, ) from exc except Exception as exc: # noqa: BLE001 logger.exception(f"Responses generation failed: {exc}") await finish_response( prepared.row.id, status="failed", output_items=[], error={"code": "internal_error", "message": "Response generation failed"}, ) raise ResponsesAPIError( "Response generation failed", code="internal_error", status_code=500 ) from exc async def _stream_response( prepared: PreparedResponse, *, text_item_id: str, ) -> AsyncIterator[bytes]: queue: asyncio.Queue[dict[str, Any] | None] = asyncio.Queue() text_started = False async def on_delta(delta: str) -> None: await queue.put({"kind": "delta", "delta": delta}) async def generate() -> None: try: row = await _run_prepared( prepared, on_delta=on_delta, text_item_id=text_item_id, ) await queue.put({"kind": "completed", "row": row}) except ResponsesAPIError as exc: await queue.put({"kind": "failed", "error": exc}) finally: await queue.put(None) task = asyncio.create_task(generate(), name=f"stream-{prepared.row.id}") try: created = response_payload(prepared.row) yield _sse({"type": "response.created", "response": created}) yield _sse({"type": "response.in_progress", "response": created}) while True: event = await queue.get() if event is None: break if event["kind"] == "delta": if not text_started: text_started = True pending_item = _text_item(text_item_id, "") pending_item["status"] = "in_progress" yield _sse( { "type": "response.output_item.added", "output_index": 0, "item": pending_item, } ) yield _sse( { "type": "response.content_part.added", "item_id": text_item_id, "output_index": 0, "content_index": 0, "part": { "type": "output_text", "text": "", "annotations": [], }, } ) yield _sse( { "type": "response.output_text.delta", "item_id": text_item_id, "output_index": 0, "content_index": 0, "delta": event["delta"], } ) continue if event["kind"] == "failed": error: ResponsesAPIError = event["error"] yield _sse({"type": "error", **error_payload(error)}) failed = await get_response( prepared.row.id, api_key_id=prepared.row.api_key_id ) yield _sse( {"type": "response.failed", "response": response_payload(failed)} ) continue row = event["row"] payload = response_payload(row) output_index = 0 for item in row.output_items or []: if item.get("type") == "message": text = str((item.get("content") or [{}])[0].get("text") or "") if not text_started: yield _sse( { "type": "response.output_item.added", "output_index": output_index, "item": { **item, "status": "in_progress", "content": [], }, } ) yield _sse( { "type": "response.content_part.added", "item_id": item["id"], "output_index": output_index, "content_index": 0, "part": { "type": "output_text", "text": "", "annotations": [], }, } ) yield _sse( { "type": "response.output_text.done", "item_id": item["id"], "output_index": output_index, "content_index": 0, "text": text, } ) yield _sse( { "type": "response.content_part.done", "item_id": item["id"], "output_index": output_index, "content_index": 0, "part": item["content"][0], } ) yield _sse( { "type": "response.output_item.done", "output_index": output_index, "item": item, } ) else: yield _sse( { "type": "response.output_item.added", "output_index": output_index, "item": item, } ) yield _sse( { "type": "response.output_item.done", "output_index": output_index, "item": item, } ) output_index += 1 yield _sse({"type": "response.completed", "response": payload}) finally: if not task.done(): task.cancel() await asyncio.gather(task, return_exceptions=True) @router.post("") async def create_response(request: Request): try: credential = await _credential(request) body = await request.json() parsed: ResponseRequest = parse_request(body) prepared = await prepare_response(parsed, api_key_id=credential.api_key_id) text_item_id = new_id("msg") if parsed.stream: return StreamingResponse( _stream_response(prepared, text_item_id=text_item_id), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) row = await _run_prepared(prepared, text_item_id=text_item_id) return response_payload(row) except RealtimeAuthError as exc: return _json_error( ResponsesAPIError(str(exc), code="invalid_api_key", status_code=401) ) except (json.JSONDecodeError, UnicodeDecodeError): return _json_error(ResponsesAPIError("Request body must be valid JSON")) except ResponsesAPIError as exc: return _json_error(exc) except ValueError as exc: return _json_error(ResponsesAPIError(str(exc))) @router.get("/{response_id}") async def retrieve_response(response_id: str, request: Request): try: credential = await _credential(request) row = await get_response(response_id, api_key_id=credential.api_key_id) return response_payload(row) except RealtimeAuthError as exc: return _json_error( ResponsesAPIError(str(exc), code="invalid_api_key", status_code=401) ) except ResponsesAPIError as exc: return _json_error(exc)