| """FastAPI streaming response wrappers for public API wire formats.""" |
|
|
| import asyncio |
| from collections.abc import ( |
| AsyncIterator, |
| Awaitable, |
| Callable, |
| Mapping, |
| ) |
| from typing import Any, Literal |
|
|
| from fastapi.responses import JSONResponse, Response, StreamingResponse |
| from starlette.background import BackgroundTask |
| from starlette.responses import ContentStream |
| from starlette.types import Receive, Scope, Send |
|
|
| from free_claude_code.core.anthropic import anthropic_error_type_for_failure |
| from free_claude_code.core.anthropic.streaming import ( |
| ANTHROPIC_SSE_RESPONSE_HEADERS, |
| anthropic_terminal_error_frame, |
| anthropic_terminal_failure_frame, |
| ) |
| from free_claude_code.core.async_iterators import try_close_async_iterator |
| from free_claude_code.core.diagnostics import safe_exception_message |
| from free_claude_code.core.failures import find_execution_failure |
| from free_claude_code.core.trace import close_stream_input, trace_event |
|
|
| TERMINAL_EXECUTION_ERROR_HEADERS = {"x-should-retry": "false"} |
|
|
| PreStartErrorResponse = Callable[[BaseException], Response] |
| TerminalFrameEmitter = Callable[[BaseException], str] |
| TerminalFailureObserver = Callable[[BaseException], None] |
| ReleaseResponseResource = Callable[[], Awaitable[None]] |
| WireApi = Literal["messages", "responses"] |
|
|
|
|
| class EmptyStreamError(RuntimeError): |
| """Raised when a public stream ends before emitting any protocol chunk.""" |
|
|
|
|
| class ManagedStreamingResponse(StreamingResponse): |
| """Own body closure and one response-scoped runtime release callback.""" |
|
|
| def __init__( |
| self, |
| content: ContentStream, |
| status_code: int = 200, |
| headers: Mapping[str, str] | None = None, |
| media_type: str | None = None, |
| background: BackgroundTask | None = None, |
| ) -> None: |
| super().__init__( |
| content, |
| status_code=status_code, |
| headers=headers, |
| media_type=media_type, |
| background=background, |
| ) |
| self._release: ReleaseResponseResource | None = None |
| self._cleanup_task: asyncio.Task[None] | None = None |
|
|
| def bind_release(self, release: ReleaseResponseResource) -> None: |
| """Bind the resource retained for this response before ASGI execution.""" |
| if self._release is not None: |
| raise RuntimeError("A response resource release is already bound.") |
| if self._cleanup_task is not None: |
| raise RuntimeError("Cannot bind a resource after response cleanup started.") |
| self._release = release |
|
|
| async def aclose(self) -> None: |
| """Close the body and release its runtime resource exactly once.""" |
| await self._close(preserved_error=None) |
|
|
| async def _close(self, *, preserved_error: BaseException | None) -> None: |
| task = self._cleanup_task |
| if task is None: |
| task = asyncio.create_task( |
| self._cleanup(preserved_error=preserved_error), |
| name="fcc-api-response-cleanup", |
| ) |
| self._cleanup_task = task |
| await _wait_for_cleanup(task) |
|
|
| async def __call__( |
| self, |
| scope: Scope, |
| receive: Receive, |
| send: Send, |
| ) -> None: |
| preserved_error: BaseException | None = None |
| try: |
| await super().__call__(scope, receive, send) |
| except BaseException as exc: |
| preserved_error = exc |
| raise |
| finally: |
| await self._close(preserved_error=preserved_error) |
|
|
| async def _cleanup(self, *, preserved_error: BaseException | None) -> None: |
| try: |
| await close_stream_input( |
| self.body_iterator, |
| owner="ManagedStreamingResponse", |
| source="api", |
| preserved_error=preserved_error, |
| ) |
| except Exception as exc: |
| _trace_response_cleanup_failure("close_body", exc) |
|
|
| release = self._release |
| if release is None: |
| return |
| try: |
| await release() |
| except Exception as exc: |
| _trace_response_cleanup_failure("release_resource", exc) |
|
|
|
|
| async def _wait_for_cleanup(task: asyncio.Task[None]) -> None: |
| """Wait through repeated caller cancellation, then restore cancellation.""" |
| cancellation: asyncio.CancelledError | None = None |
| while not task.done(): |
| try: |
| await asyncio.shield(task) |
| except asyncio.CancelledError as exc: |
| cancellation = exc |
|
|
| |
| try: |
| task.result() |
| except asyncio.CancelledError: |
| if cancellation is not None: |
| raise cancellation from None |
| raise |
| except Exception as exc: |
| _trace_response_cleanup_failure("cleanup_task", exc) |
|
|
| if cancellation is not None: |
| raise cancellation |
|
|
|
|
| def _trace_response_cleanup_failure(operation: str, exc: BaseException) -> None: |
| trace_event( |
| stage="egress", |
| event="free_claude_code.api.response.cleanup_failed", |
| source="api", |
| operation=operation, |
| exc_type=type(exc).__name__, |
| ) |
|
|
|
|
| async def bind_response_lifetime( |
| response: object, |
| release: ReleaseResponseResource, |
| ) -> object: |
| """Retain a runtime resource until a response body is fully consumed.""" |
| if isinstance(response, ManagedStreamingResponse): |
| response.bind_release(release) |
| return response |
| if isinstance(response, StreamingResponse): |
| error = TypeError("Streaming API responses must use ManagedStreamingResponse.") |
| try: |
| await close_stream_input( |
| response.body_iterator, |
| owner="bind_response_lifetime", |
| source="api", |
| preserved_error=error, |
| ) |
| finally: |
| await release() |
| raise error |
| await release() |
| return response |
|
|
|
|
| def terminal_execution_error_response( |
| *, status_code: int, content: dict[str, Any] |
| ) -> JSONResponse: |
| """Return a final provider-execution error without enabling client retries.""" |
| return JSONResponse( |
| status_code=status_code, |
| content=content, |
| headers=dict(TERMINAL_EXECUTION_ERROR_HEADERS), |
| ) |
|
|
|
|
| def trace_terminal_execution_error( |
| *, |
| wire_api: WireApi, |
| request_id: str, |
| status_code: int, |
| error_type: str, |
| error: BaseException | None = None, |
| ) -> None: |
| """Record one correlated terminal-execution decision at the HTTP boundary.""" |
| fields: dict[str, object] = { |
| "stage": "egress", |
| "event": "free_claude_code.api.response.terminal_execution_error", |
| "source": "api", |
| "wire_api": wire_api, |
| "request_id": request_id, |
| "status_code": status_code, |
| "error_type": error_type, |
| "client_should_retry": False, |
| } |
| failure = find_execution_failure(error) if error is not None else None |
| if error is not None: |
| fields["exc_type"] = type(failure or error).__name__ |
| if failure is not None: |
| fields["failure_kind"] = failure.kind.value |
| fields["provider_retryable"] = failure.retryable |
| trace_event(**fields) |
|
|
|
|
| async def _first_chunk_streaming_response( |
| body: AsyncIterator[str], |
| *, |
| headers: Mapping[str, str], |
| pre_start_error_response: PreStartErrorResponse, |
| terminal_frame: TerminalFrameEmitter | None, |
| terminal_failure_observer: TerminalFailureObserver | None, |
| ) -> Response: |
| try: |
| first_chunk = await anext(body) |
| except StopAsyncIteration: |
| error = EmptyStreamError("Stream ended before emitting a response.") |
| await _close_pre_start_body(body, preserved_error=error) |
| return pre_start_error_response(error) |
| except GeneratorExit as exc: |
| await _close_pre_start_body(body, preserved_error=exc) |
| raise |
| except asyncio.CancelledError as exc: |
| await _close_pre_start_body(body, preserved_error=exc) |
| raise |
| except BaseExceptionGroup as exc: |
| await _close_pre_start_body(body, preserved_error=exc) |
| return pre_start_error_response(exc) |
| except Exception as exc: |
| await _close_pre_start_body(body, preserved_error=exc) |
| return pre_start_error_response(exc) |
|
|
| return ManagedStreamingResponse( |
| _PrefetchedStream( |
| first_chunk, |
| body, |
| terminal_frame=terminal_frame, |
| terminal_failure_observer=terminal_failure_observer, |
| ), |
| media_type="text/event-stream", |
| headers=dict(headers), |
| ) |
|
|
|
|
| async def _close_pre_start_body( |
| body: AsyncIterator[str], |
| *, |
| preserved_error: BaseException, |
| ) -> None: |
| task = asyncio.create_task( |
| close_stream_input( |
| body, |
| owner="first_chunk_streaming_response", |
| source="api", |
| preserved_error=preserved_error, |
| ), |
| name="fcc-api-pre-start-stream-cleanup", |
| ) |
| await _wait_for_cleanup(task) |
|
|
|
|
| class _PrefetchedStream(AsyncIterator[str]): |
| """Replay one prefetched frame while retaining ownership of the tail.""" |
|
|
| def __init__( |
| self, |
| first_chunk: str, |
| body: AsyncIterator[str], |
| *, |
| terminal_frame: TerminalFrameEmitter | None, |
| terminal_failure_observer: TerminalFailureObserver | None, |
| ) -> None: |
| self._first_chunk: str | None = first_chunk |
| self._body = body |
| self._terminal_frame = terminal_frame |
| self._terminal_failure_observer = terminal_failure_observer |
| self._done = False |
| self._closed = False |
|
|
| def __aiter__(self) -> _PrefetchedStream: |
| return self |
|
|
| async def __anext__(self) -> str: |
| if self._closed or self._done: |
| raise StopAsyncIteration |
| if self._first_chunk is not None: |
| first_chunk = self._first_chunk |
| self._first_chunk = None |
| return first_chunk |
| try: |
| return await anext(self._body) |
| except StopAsyncIteration: |
| self._done = True |
| raise |
| except BaseExceptionGroup as exc: |
| return self._terminal_chunk(find_execution_failure(exc) or exc) |
| except Exception as exc: |
| return self._terminal_chunk(exc) |
|
|
| async def aclose(self) -> None: |
| if self._closed: |
| return |
| self._closed = True |
| self._done = True |
| close_error = await try_close_async_iterator(self._body) |
| if close_error is not None: |
| raise close_error |
|
|
| def _terminal_chunk(self, exc: BaseException) -> str: |
| terminal_frame = self._terminal_frame |
| if terminal_frame is None: |
| raise exc |
| self._done = True |
| if self._terminal_failure_observer is not None: |
| self._terminal_failure_observer(exc) |
| return terminal_frame(exc) |
|
|
|
|
| async def anthropic_sse_streaming_response( |
| body: AsyncIterator[str], |
| *, |
| pre_start_error_response: PreStartErrorResponse, |
| request_id: str, |
| ) -> Response: |
| """Return a streaming response for Anthropic-style SSE streams.""" |
| return await _first_chunk_streaming_response( |
| body, |
| headers=ANTHROPIC_SSE_RESPONSE_HEADERS, |
| pre_start_error_response=pre_start_error_response, |
| terminal_frame=_anthropic_terminal_frame, |
| terminal_failure_observer=lambda exc: _trace_anthropic_terminal_failure( |
| exc, |
| request_id=request_id, |
| ), |
| ) |
|
|
|
|
| def _anthropic_terminal_frame(exc: BaseException) -> str: |
| failure = find_execution_failure(exc) |
| if failure is not None: |
| return anthropic_terminal_failure_frame(failure) |
| return anthropic_terminal_error_frame(safe_exception_message(exc)) |
|
|
|
|
| def _trace_anthropic_terminal_failure( |
| exc: BaseException, |
| *, |
| request_id: str, |
| ) -> None: |
| failure = find_execution_failure(exc) |
| trace_terminal_execution_error( |
| wire_api="messages", |
| request_id=request_id, |
| status_code=failure.status_code if failure is not None else 500, |
| error_type=( |
| anthropic_error_type_for_failure(failure) |
| if failure is not None |
| else "api_error" |
| ), |
| error=exc, |
| ) |
|
|
|
|
| async def openai_responses_sse_streaming_response( |
| body: AsyncIterator[str], |
| *, |
| headers: Mapping[str, str], |
| pre_start_error_response: PreStartErrorResponse, |
| ) -> Response: |
| """Return a streaming response for OpenAI Responses-style SSE.""" |
| return await _first_chunk_streaming_response( |
| body, |
| headers=headers, |
| pre_start_error_response=pre_start_error_response, |
| terminal_frame=None, |
| terminal_failure_observer=None, |
| ) |
|
|