claude-proxy / src /free_claude_code /api /response_streams.py
dheraingoud's picture
fix(compatibility): resolve python 3.12 compatibility NameErrors and SyntaxErrors
05e7f80
Raw
History Blame Contribute Delete
12.7 kB
"""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
# Ordinary defensive failures are trace-only; cancellation remains control flow.
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,
)