free-claude-code/api/handlers/messages.py
Ali Khokhar 85b601884d
Some checks are pending
CI / Ban suppressions and legacy annotations (push) Waiting to run
CI / pytest (push) Waiting to run
CI / ruff-check (push) Waiting to run
CI / ruff-format (push) Waiting to run
CI / ty (push) Waiting to run
Remove legacy future annotation imports (#982)
## Problem

Python 3.14 provides native lazy annotations, but the codebase still
relied on legacy future annotation imports. Those imports also made
type-only import cycles easier to hide instead of fixing ownership
boundaries.

## Changes

| Before | After |
| --- | --- |
| Python files used `from __future__ import annotations`. | Python files
rely on Python 3.14 native lazy annotations. |
| Some runtime modules used `TYPE_CHECKING` or local imports for
required dependencies. | Runtime modules use top-level owner-module
imports with explicit boundaries. |
| Local and GitHub guardrails only rejected type ignore suppressions. |
Local and GitHub guardrails reject type ignore suppressions and legacy
future annotation imports. |
| Agent docs only documented the no-type-ignore rule. | Agent docs
document the Python 3.14 annotation and import-boundary rules. |

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR moves the codebase to Python 3.14 native lazy annotations. The
main changes are:

- Removed legacy `from __future__ import annotations` imports across
Python modules.
- Promoted selected runtime dependencies from `TYPE_CHECKING` or local
imports to explicit owner-module imports.
- Added local, GitHub, and contract-test guardrails to reject legacy
future annotation imports.
- Updated agent docs with the annotation and import-boundary rules.
- Bumped the package patch version for production-file changes.
</details>

<h3>Confidence Score: 5/5</h3>

Safe to merge with low risk.

The changes are mostly mechanical annotation cleanup with matching CI
and contract-test guardrails. Reviewed import-boundary updates did not
show a confirmed runtime cycle or dependency break.

No files require special attention.

<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>

**What T-Rex did**
- Performed an end-to-end validation of the guardrail contract suite: an
environment check confirmed uv availability, a guardrail pytest run used
CPython 3.14.0 with 5 passing contract tests, 3 focused CI-script tests
passed, and the direct CI suppressions guardrail command (including the
legacy future-annotations grep) also passed.

<a
href="https://app.greptile.com/trex/runs/13303335/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a>

<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>

<details open><summary><h3>Important Files Changed</h3></summary>

| Filename | Overview |
|----------|----------|
| api/runtime.py | Moves messaging, CLI manager, session, limiter, and
tree dependencies from local/type-checking imports to explicit top-level
owner-module imports. |
| messaging/platforms/telegram.py | Removes future annotations and
promotes Telegram SDK type imports into the existing availability guard.
|
| messaging/platforms/telegram_inbound.py | Removes future annotations
and imports Telegram SDK types at module scope for inbound
normalization. |
| tests/contracts/test_import_boundaries.py | Adds an AST contract that
rejects legacy future annotation imports across Python files. |
| scripts/ci.sh | Extends the local suppression check to reject legacy
future annotation imports alongside type-ignore suppressions. |
| scripts/ci.ps1 | Mirrors the local PowerShell CI suppression check for
legacy future annotations. |
| .github/workflows/tests.yml | Renames and broadens the GitHub
guardrail job to reject both type suppressions and legacy future
annotations. |
| pyproject.toml | Bumps the patch version for production-file changes.
|

</details>

<details open><summary><h3>Sequence Diagram</h3></summary>

<a href="#gh-light-mode-only">

```mermaid
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Dev as Developer/CI
participant Guard as Suppression guard
participant AST as Import-boundary contract test
participant Py as Python modules

Dev->>Guard: Run local/GitHub suppression check
Guard->>Py: "Scan *.py for type ignores and future annotations"
Guard-->>Dev: Fail if legacy annotation import remains
Dev->>AST: Run pytest contract tests
AST->>Py: Parse imports with ast
AST-->>Dev: Assert no future annotations/import-boundary violations
Py-->>Dev: Use Python 3.14 native lazy annotations
```

</a>
<a href="#gh-dark-mode-only">

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Dev as Developer/CI
participant Guard as Suppression guard
participant AST as Import-boundary contract test
participant Py as Python modules

Dev->>Guard: Run local/GitHub suppression check
Guard->>Py: "Scan *.py for type ignores and future annotations"
Guard-->>Dev: Fail if legacy annotation import remains
Dev->>AST: Run pytest contract tests
AST->>Py: Parse imports with ast
AST-->>Dev: Assert no future annotations/import-boundary violations
Py-->>Dev: Use Python 3.14 native lazy annotations
```

</a>
</details>

<sub>Reviews (2): Last reviewed commit: ["Remove legacy future
annotations
import"](6e6cda69da)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=41875785)</sub>

<!-- /greptile_comment -->
2026-07-04 21:41:51 -07:00

204 lines
7.6 KiB
Python

"""Claude Messages API product flow."""
from collections.abc import AsyncIterator, Callable
from dataclasses import dataclass, replace
from fastapi.responses import JSONResponse
from loguru import logger
from api.detection import is_safety_classifier_request
from api.model_router import ModelRouter, RoutedMessagesRequest
from api.models.anthropic import MessagesRequest
from api.optimization_handlers import try_optimizations
from api.provider_execution import ProviderExecutionService, TokenCounter
from api.request_errors import require_non_empty_messages, unexpected_http_exception
from api.response_streams import anthropic_sse_streaming_response
from api.web_tools.egress import WebFetchEgressPolicy, web_fetch_allowed_scheme_set
from api.web_tools.request import (
is_web_server_tool_request,
openai_chat_upstream_server_tool_error,
)
from api.web_tools.streaming import stream_web_server_tool_response
from config.provider_catalog import PROVIDER_CATALOG
from config.settings import Settings
from core.anthropic import aggregate_anthropic_sse_to_message, get_token_count
from core.trace import trace_event
from providers.base import BaseProvider
from providers.exceptions import InvalidRequestError, ProviderError
_OPENAI_CHAT_UPSTREAM_IDS = frozenset(
provider_id
for provider_id, descriptor in PROVIDER_CATALOG.items()
if descriptor.transport_type == "openai_chat"
)
@dataclass(frozen=True)
class _MessagesStreamResult:
body: AsyncIterator[str]
@dataclass(frozen=True)
class _MessagesCompleteResult:
response: object
ProviderGetter = Callable[[str], BaseProvider]
_MessagesResult = _MessagesStreamResult | _MessagesCompleteResult
MessageIntercept = Callable[[RoutedMessagesRequest], _MessagesResult | None]
class MessagesHandler:
"""Handle Anthropic-compatible Messages requests."""
def __init__(
self,
settings: Settings,
provider_getter: ProviderGetter,
*,
model_router: ModelRouter | None = None,
token_counter: TokenCounter = get_token_count,
provider_execution: ProviderExecutionService | None = None,
) -> None:
self._settings = settings
self._model_router = model_router or ModelRouter(settings)
self._token_counter = token_counter
self._provider_execution = provider_execution or ProviderExecutionService(
settings,
provider_getter,
token_counter=token_counter,
)
self._message_intercepts: tuple[MessageIntercept, ...] = (
self._intercept_web_server_tool,
self._intercept_local_optimization,
)
async def create(self, request_data: MessagesRequest) -> object:
"""Create an Anthropic-compatible message response."""
try:
require_non_empty_messages(request_data.messages)
routed = self._model_router.resolve_messages_request(request_data)
routed = self._apply_message_routing_policies(routed)
self._reject_unsupported_server_tools(routed)
result = self._run_message_intercepts(routed)
if result is None:
logger.debug("No optimization matched, routing to provider")
result = _MessagesStreamResult(
self._provider_execution.stream(
routed,
wire_api="messages",
raw_log_label="FULL_PAYLOAD",
raw_log_payload=routed.request.model_dump(),
)
)
return await self._to_public_response(result, stream=request_data.stream)
except ProviderError:
raise
except Exception as exc:
raise unexpected_http_exception(
self._settings, exc, context="CREATE_MESSAGE_ERROR"
) from exc
async def _to_public_response(
self, result: _MessagesResult, *, stream: bool | None
) -> object:
if isinstance(result, _MessagesCompleteResult):
return result.response
if stream is False:
# Non-streaming clients (e.g. Claude Code utility calls) need a
# complete JSON Message; the internal pipeline is always SSE, so
# serving that raw here breaks the client SDK's response parse.
message, error = await aggregate_anthropic_sse_to_message(result.body)
if error is not None and not message.get("content"):
return JSONResponse(
status_code=502,
content={"type": "error", "error": error},
)
return JSONResponse(content=message)
return anthropic_sse_streaming_response(result.body)
def _reject_unsupported_server_tools(self, routed: RoutedMessagesRequest) -> None:
if routed.resolved.provider_id not in _OPENAI_CHAT_UPSTREAM_IDS:
return
tool_err = openai_chat_upstream_server_tool_error(
routed.request,
web_tools_enabled=self._settings.enable_web_server_tools,
)
if tool_err is not None:
raise InvalidRequestError(tool_err)
def _apply_message_routing_policies(
self, routed: RoutedMessagesRequest
) -> RoutedMessagesRequest:
if not is_safety_classifier_request(routed.request):
return routed
changed = routed.resolved.thinking_enabled
trace_event(
stage="routing",
event="api.optimization.safety_classifier_no_thinking",
source="api",
model=routed.request.model,
changed=changed,
)
if not changed:
return routed
return RoutedMessagesRequest(
request=routed.request,
resolved=replace(routed.resolved, thinking_enabled=False),
)
def _run_message_intercepts(
self, routed: RoutedMessagesRequest
) -> _MessagesResult | None:
for intercept in self._message_intercepts:
result = intercept(routed)
if result is not None:
return result
return None
def _intercept_web_server_tool(
self, routed: RoutedMessagesRequest
) -> _MessagesResult | None:
if not self._settings.enable_web_server_tools:
return None
if not is_web_server_tool_request(routed.request):
return None
input_tokens = self._token_counter(
routed.request.messages, routed.request.system, routed.request.tools
)
trace_event(
stage="routing",
event="api.optimization.web_server_tool",
source="api",
model=routed.request.model,
)
egress = WebFetchEgressPolicy(
allow_private_network_targets=self._settings.web_fetch_allow_private_networks,
allowed_schemes=web_fetch_allowed_scheme_set(
self._settings.web_fetch_allowed_schemes
),
)
return _MessagesStreamResult(
stream_web_server_tool_response(
routed.request,
input_tokens=input_tokens,
web_fetch_egress=egress,
verbose_client_errors=self._settings.log_api_error_tracebacks,
),
)
def _intercept_local_optimization(
self, routed: RoutedMessagesRequest
) -> _MessagesResult | None:
optimized = try_optimizations(routed.request, self._settings)
if optimized is None:
return None
trace_event(
stage="routing",
event="api.optimization.short_circuit",
source="api",
model=routed.request.model,
)
return _MessagesCompleteResult(optimized)