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:
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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user