feat: Anthropic Messages API compat (/v1/messages)
Add a wire-compatible Anthropic endpoint alongside the existing OpenAI one
so Claude Code / anthropic-sdk / Cursor Agent can hit Lingma directly.
- app/anthropic_schema.py (new): request model + content-block flattener
+ internal-messages adapter + affinity key helper. Handles text / image /
tool_use / tool_result blocks; unknown types degrade gracefully.
- app/auth.py: add require_anthropic_key (x-api-key, Bearer fallback)
and AnthropicAuthError so auth failures render in Anthropic's error
envelope instead of FastAPI's {detail:...} wrapper.
- app/main.py: POST /v1/messages. Shares LingmaPool / SessionCache /
InFlightGuard / StatsCollector with the OpenAI path — same api_key +
same conversation prefix hits the same upstream sessionId across both
protocols (KV cache carries over). Streaming emits the named Anthropic
event sequence (message_start / content_block_start / content_block_delta
/ content_block_stop / message_delta / message_stop). No claude-*
model mapping table: resolve_model's default fallback handles it.
- README.md / DESIGN.md: document the new endpoint, add decision 5.12,
iteration history M5, and a 4.3b streaming flow diagram.
- Bump FastAPI app version to 0.4.0.
Made-with: Cursor
This commit is contained in:
52
app/auth.py
52
app/auth.py
@@ -98,6 +98,58 @@ def require_metrics_access(
|
||||
)
|
||||
|
||||
|
||||
class AnthropicAuthError(Exception):
|
||||
"""Raised when an Anthropic Messages request fails authentication.
|
||||
|
||||
Carries enough context for the endpoint to render the Anthropic-shaped
|
||||
error body (`{"type":"error","error":{"type":..., "message":...}}`) — we
|
||||
don't use `HTTPException` here because FastAPI would wrap the detail in
|
||||
`{"detail": ...}`, which is not the Anthropic wire format.
|
||||
"""
|
||||
|
||||
def __init__(self, status_code: int, error_type: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
self.error_type = error_type
|
||||
self.message = message
|
||||
|
||||
|
||||
def require_anthropic_key(request: Request, api_keys: list[str]) -> None:
|
||||
"""Authenticate a `POST /v1/messages` request the Anthropic way.
|
||||
|
||||
Accept order:
|
||||
1. `x-api-key` header (official Anthropic SDK / CLI / Claude Code)
|
||||
2. `Authorization: Bearer <token>` (OpenAI-shaped clients / curl)
|
||||
|
||||
Empty `api_keys` means auth is disabled — the startup auth-posture warning
|
||||
already covers that case loudly, same as `require_bearer`.
|
||||
|
||||
Note: we keep `anthropic-version` header permissive (don't parse/validate)
|
||||
so clients on any official version work without gateway churn.
|
||||
"""
|
||||
if not api_keys:
|
||||
return
|
||||
|
||||
token = request.headers.get("x-api-key", "").strip()
|
||||
if not token:
|
||||
auth = request.headers.get("authorization", "")
|
||||
if auth.startswith("Bearer "):
|
||||
token = auth[len("Bearer ") :].strip()
|
||||
|
||||
if not token:
|
||||
raise AnthropicAuthError(
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
"authentication_error",
|
||||
"missing x-api-key header (or Authorization: Bearer ...)",
|
||||
)
|
||||
if not _match_any(token, api_keys):
|
||||
raise AnthropicAuthError(
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
"authentication_error",
|
||||
"invalid x-api-key",
|
||||
)
|
||||
|
||||
|
||||
def require_admin_access(
|
||||
request: Request,
|
||||
api_keys: list[str],
|
||||
|
||||
Reference in New Issue
Block a user