Fix Anthropic Messages API compatibility (#25876)

Co-authored-by: Jairo David Campaña Rosero <jairocampana10001@gmail.com>
Co-authored-by: Karan Bansal <3264937+karanb192@users.noreply.github.com>
Co-authored-by: eason <85663565+mango766@users.noreply.github.com>
Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: qingchanghan <17794466+qingchanghan@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ajay Anubolu <124525760+AjAnubolu@users.noreply.github.com>
Co-authored-by: Ravitez Dondeti <13931987+dondetir@users.noreply.github.com>
Co-authored-by: Ratish P <114130421+Ratish1@users.noreply.github.com>
Co-authored-by: Xiaoshuai Zhang <15795935+jetd1@users.noreply.github.com>
Co-authored-by: Ricardo-M-L <69202550+Ricardo-M-L@users.noreply.github.com>
Co-authored-by: Xinyuan Tong <xinyuan.tong@radixark.ai>
This commit is contained in:
Xinyuan Tong
2026-06-12 14:46:57 -07:00
committed by GitHub
co-authored by Jairo David Campaña Rosero Karan Bansal eason Yufeng He qingchanghan Claude Opus 4.7 Ajay Anubolu Ravitez Dondeti Ratish P Xiaoshuai Zhang Ricardo-M-L Xinyuan Tong
parent caf59759ea
commit b3270264e4
5 changed files with 2791 additions and 299 deletions
+99 -19
View File
@@ -425,13 +425,77 @@ from sglang.srt.entrypoints.v1_loads import router as v1_loads_router
app.include_router(v1_loads_router)
def _anthropic_validation_message(raw_errors) -> str:
"""Render Pydantic-style errors for an Anthropic /v1/messages route.
Builds a short ``loc: msg`` digest that names the offending fields without
leaking file paths or Python internals (the default ``str(exc)`` includes
the dispatcher's ``File "/.../http_server.py"`` line).
"""
parts: list[str] = []
for err in raw_errors or []:
loc = err.get("loc") or ()
if loc:
loc_str = ".".join(str(p) for p in loc if p not in ("body",))
else:
loc_str = ""
msg = (err.get("msg") or "").strip()
if loc_str and msg:
parts.append(f"{loc_str}: {msg}")
elif msg:
parts.append(msg)
text = "; ".join(parts) or "Invalid request"
if len(text) > 500:
text = text[:500] + "…"
return text
def _anthropic_error_response(*, status_code: int, error_type: str, message: str):
"""Anthropic-format error envelope: {"type":"error","error":{"type":...,"message":...}}."""
return ORJSONResponse(
status_code=status_code,
content={
"type": "error",
"error": {"type": error_type, "message": message},
},
)
@app.exception_handler(HTTPException)
async def validation_exception_handler(request: Request, exc: HTTPException):
"""Enrich HTTP exception with status code and other details.
For /v1/responses, emit OpenAI-style nested error envelope:
{"error": {"message": "...", "type": "...", "param": null, "code": <status>}}
For /v1/messages, emit Anthropic-style envelope so SDK clients can parse it.
"""
if request.url.path.startswith("/v1/messages"):
# Map HTTP status to Anthropic error.type; fall back to api_error.
anthropic_type = {
400: "invalid_request_error",
401: "authentication_error",
403: "permission_error",
404: "not_found_error",
413: "request_too_large",
422: "invalid_request_error",
429: "rate_limit_error",
500: "api_error",
502: "api_error",
503: "overloaded_error",
504: "api_error",
}.get(exc.status_code, "api_error")
# 5xx must never echo upstream detail (may contain stack/PII).
message = (
"Internal server error"
if exc.status_code >= 500
else (str(exc.detail) if exc.detail else "Request failed")
)
return _anthropic_error_response(
status_code=exc.status_code,
error_type=anthropic_type,
message=message,
)
# adjust fmt for responses api
if request.url.path.startswith("/v1/responses"):
nested_error = {
@@ -458,8 +522,18 @@ async def validation_exception_handler(request: Request, exc: HTTPException):
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Override FastAPI's default 422 validation error with 400.
For /v1/responses, emit OpenAI-style nested error envelope; for other endpoints keep legacy format.
For /v1/messages, emit Anthropic-style envelope and scrub the message so
file paths or Python internals from the default ``str(exc)`` representation
never reach the client. For /v1/responses, keep OpenAI-style. Otherwise
use the legacy ErrorResponse shape.
"""
if request.url.path.startswith("/v1/messages"):
return _anthropic_error_response(
status_code=HTTPStatus.BAD_REQUEST.value,
error_type="invalid_request_error",
message=_anthropic_validation_message(exc.errors()),
)
exc_str = str(exc)
errors_str = str(exc.errors())
@@ -1067,9 +1141,11 @@ async def dump_expert_distribution_record_async():
@auth_level(AuthLevel.ADMIN_OPTIONAL)
async def update_weights_from_disk(obj: UpdateWeightFromDiskReqInput, request: Request):
"""Update the weights from disk inplace without re-launching the server."""
success, message, num_paused_requests = (
await _global_state.tokenizer_manager.update_weights_from_disk(obj, request)
)
(
success,
message,
num_paused_requests,
) = await _global_state.tokenizer_manager.update_weights_from_disk(obj, request)
content = {
"success": success,
@@ -1093,10 +1169,11 @@ async def update_weights_from_disk(obj: UpdateWeightFromDiskReqInput, request: R
async def init_weights_send_group_for_remote_instance(
obj: InitWeightsSendGroupForRemoteInstanceReqInput, request: Request
):
success, message = (
await _global_state.tokenizer_manager.init_weights_send_group_for_remote_instance(
obj, request
)
(
success,
message,
) = await _global_state.tokenizer_manager.init_weights_send_group_for_remote_instance(
obj, request
)
content = {"success": success, "message": message}
if success:
@@ -1110,10 +1187,11 @@ async def init_weights_send_group_for_remote_instance(
async def send_weights_to_remote_instance(
obj: SendWeightsToRemoteInstanceReqInput, request: Request
):
success, message = (
await _global_state.tokenizer_manager.send_weights_to_remote_instance(
obj, request
)
(
success,
message,
) = await _global_state.tokenizer_manager.send_weights_to_remote_instance(
obj, request
)
content = {"success": success, "message": message}
if success:
@@ -1182,9 +1260,10 @@ async def destroy_weights_update_group(
obj: DestroyWeightsUpdateGroupReqInput, request: Request
):
"""Destroy the parameter update group."""
success, message = (
await _global_state.tokenizer_manager.destroy_weights_update_group(obj, request)
)
(
success,
message,
) = await _global_state.tokenizer_manager.destroy_weights_update_group(obj, request)
content = {"success": success, "message": message}
return ORJSONResponse(
content, status_code=200 if success else HTTPStatus.BAD_REQUEST
@@ -1219,10 +1298,11 @@ async def update_weights_from_distributed(
obj: UpdateWeightsFromDistributedReqInput, request: Request
):
"""Update model parameter from distributed online."""
success, message = (
await _global_state.tokenizer_manager.update_weights_from_distributed(
obj, request
)
(
success,
message,
) = await _global_state.tokenizer_manager.update_weights_from_distributed(
obj, request
)
content = {"success": success, "message": message}