Skip to content

Commit 63c2eba

Browse files
authored
API: add support for Anthropic /v1/messages endpoint (#1400)
* API: add support for Anthropic /v1/messages endpoint * align endpoint logs
1 parent 8d8697a commit 63c2eba

3 files changed

Lines changed: 732 additions & 18 deletions

File tree

aphrodite/endpoints/openai/api_server.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
from aphrodite.endpoints.logger import RequestLogger
4949
from aphrodite.endpoints.openai.args import (make_arg_parser,
5050
validate_parsed_serve_args)
51-
from aphrodite.endpoints.openai.protocol import (ChatCompletionRequest,
51+
from aphrodite.endpoints.openai.protocol import (AnthropicMessagesRequest,
52+
AnthropicMessagesResponse,
53+
ChatCompletionRequest,
5254
ChatCompletionResponse,
5355
CompletionRequest,
5456
CompletionResponse,
@@ -78,6 +80,7 @@
7880
OpenAIServingCompletion)
7981
from aphrodite.endpoints.openai.serving_embedding import OpenAIServingEmbedding
8082
from aphrodite.endpoints.openai.serving_engine import OpenAIServing
83+
from aphrodite.endpoints.openai.serving_messages import OpenAIServingMessages
8184
from aphrodite.endpoints.openai.serving_models import (BaseModelPath,
8285
OpenAIServingModels)
8386
from aphrodite.endpoints.openai.serving_pooling import OpenAIServingPooling
@@ -91,12 +94,9 @@
9194
with_cancellation)
9295
from aphrodite.engine.args_tools import AsyncEngineArgs
9396
from aphrodite.engine.async_aphrodite import AsyncAphrodite
94-
from aphrodite.engine.multiprocessing import (APHRODITE_RPC_SUCCESS_STR,
95-
RPCShutdownRequest)
9697
from aphrodite.engine.multiprocessing.client import MQAphroditeEngineClient
9798
from aphrodite.engine.multiprocessing.engine import run_mp_engine
9899
from aphrodite.engine.protocol import EngineClient
99-
from aphrodite.modeling.model_loader.weight_utils import get_model_config_yaml
100100
from aphrodite.reasoning import ReasoningParserManager
101101
from aphrodite.server import serve_http
102102
from aphrodite.transformers_utils.config import (
@@ -369,6 +369,10 @@ def chat(request: Request) -> Optional[OpenAIServingChat]:
369369
return request.app.state.openai_serving_chat
370370

371371

372+
def messages(request: Request) -> Optional[OpenAIServingMessages]:
373+
return request.app.state.openai_serving_messages
374+
375+
372376
def completion(request: Request) -> Optional[OpenAIServingCompletion]:
373377
return request.app.state.openai_serving_completion
374378

@@ -532,6 +536,28 @@ async def create_chat_completion(request: ChatCompletionRequest,
532536
return StreamingResponse(content=generator, media_type="text/event-stream")
533537

534538

539+
@router.post("/v1/messages", dependencies=[Depends(validate_json_request)])
540+
@with_cancellation
541+
@load_aware_call
542+
async def create_messages(request: AnthropicMessagesRequest,
543+
raw_request: Request):
544+
handler = messages(raw_request)
545+
if handler is None:
546+
return base(raw_request).create_error_response(
547+
message="The model does not support Anthropic Messages API")
548+
549+
generator = await handler.create_message(request, raw_request)
550+
551+
if isinstance(generator, ErrorResponse):
552+
return JSONResponse(content=generator.model_dump(),
553+
status_code=generator.code)
554+
555+
elif isinstance(generator, AnthropicMessagesResponse):
556+
return JSONResponse(content=generator.model_dump())
557+
558+
return StreamingResponse(content=generator, media_type="text/event-stream")
559+
560+
535561
@router.post("/v1/completions", dependencies=[Depends(validate_json_request)])
536562
@with_cancellation
537563
@load_aware_call
@@ -1256,6 +1282,18 @@ async def init_app_state(
12561282
reasoning_parser=args.reasoning_parser,
12571283
enable_prompt_tokens_details=args.enable_prompt_tokens_details,
12581284
) if model_config.runner_type == "generate" else None
1285+
state.openai_serving_messages = OpenAIServingMessages(
1286+
engine_client,
1287+
model_config,
1288+
state.openai_serving_models,
1289+
args.response_role,
1290+
request_logger=request_logger,
1291+
chat_template=resolved_chat_template,
1292+
return_tokens_as_token_ids=args.return_tokens_as_token_ids,
1293+
enable_auto_tools=args.enable_auto_tool_choice,
1294+
tool_parser=args.tool_call_parser,
1295+
reasoning_parser=args.reasoning_parser,
1296+
) if model_config.runner_type == "generate" else None
12591297
state.openai_serving_completion = OpenAIServingCompletion(
12601298
engine_client,
12611299
model_config,
@@ -1382,49 +1420,52 @@ def _listen_addr(a: str) -> str:
13821420

13831421
if SERVE_KOBOLD_LITE_UI:
13841422
ui_url = f"{protocol}://{host_name}:{port_str}{root_path}/"
1385-
logger.info(f"Kobold Lite UI: {ui_url}")
1423+
logger.info(f"Kobold Lite UI: {ui_url}")
13861424

13871425
if not args.disable_fastapi_docs:
13881426
logger.info(
1389-
f"Documentation: {protocol}://{host_name}:{port_str}{root_path}/redoc"
1427+
f"Documentation: {protocol}://{host_name}:{port_str}{root_path}/redoc"
13901428
) # noqa: E501
13911429
logger.info(
1392-
f"Completions API: {protocol}://{host_name}:{port_str}{root_path}/v1/completions"
1430+
f"Completions API: {protocol}://{host_name}:{port_str}{root_path}/v1/completions"
1431+
) # noqa: E501
1432+
logger.info(
1433+
f"Chat API: {protocol}://{host_name}:{port_str}{root_path}/v1/chat/completions"
13931434
) # noqa: E501
13941435
logger.info(
1395-
f"Chat API: {protocol}://{host_name}:{port_str}{root_path}/v1/chat/completions"
1436+
f"Anthropic Messages API: {protocol}://{host_name}:{port_str}{root_path}/v1/messages"
13961437
) # noqa: E501
13971438
logger.info(
1398-
f"Embeddings API: {protocol}://{host_name}:{port_str}{root_path}/v1/embeddings"
1439+
f"Embeddings API: {protocol}://{host_name}:{port_str}{root_path}/v1/embeddings"
13991440
) # noqa: E501
14001441
logger.info(
1401-
f"Pooling API: {protocol}://{host_name}:{port_str}{root_path}/pooling"
1442+
f"Pooling API: {protocol}://{host_name}:{port_str}{root_path}/pooling"
14021443
) # noqa: E501
14031444
logger.info(
1404-
f"Score API: {protocol}://{host_name}:{port_str}{root_path}/score"
1445+
f"Score API: {protocol}://{host_name}:{port_str}{root_path}/score"
14051446
) # noqa: E501
14061447
logger.info(
1407-
f"Rerank API: {protocol}://{host_name}:{port_str}{root_path}/rerank"
1448+
f"Rerank API: {protocol}://{host_name}:{port_str}{root_path}/rerank"
14081449
) # noqa: E501
14091450
logger.info(
1410-
f"Rerank API v1: {protocol}://{host_name}:{port_str}{root_path}/v1/rerank"
1451+
f"Rerank API v1: {protocol}://{host_name}:{port_str}{root_path}/v1/rerank"
14111452
) # noqa: E501
14121453
logger.info(
1413-
f"Rerank API v2: {protocol}://{host_name}:{port_str}{root_path}/v2/rerank"
1454+
f"Rerank API v2: {protocol}://{host_name}:{port_str}{root_path}/v2/rerank"
14141455
) # noqa: E501
14151456
logger.info(
1416-
f"Transcription API: {protocol}://{host_name}:{port_str}{root_path}/v1/audio/transcriptions"
1457+
f"Transcription API: {protocol}://{host_name}:{port_str}{root_path}/v1/audio/transcriptions"
14171458
) # noqa: E501
14181459
logger.info(
1419-
f"Tokenization API: {protocol}://{host_name}:{port_str}{root_path}/v1/tokenize"
1460+
f"Tokenization API: {protocol}://{host_name}:{port_str}{root_path}/v1/tokenize"
14201461
) # noqa: E501
14211462

14221463
if envs.APHRODITE_KOBOLD_API:
14231464
logger.info(
1424-
f"KoboldAI API: {protocol}://{host_name}:{port_str}{root_path}/api/v1"
1465+
f"KoboldAI API: {protocol}://{host_name}:{port_str}{root_path}/api/v1"
14251466
) # noqa: E501
14261467
logger.info(
1427-
f"KoboldAI Extra: {protocol}://{host_name}:{port_str}{root_path}/api/extra"
1468+
f"KoboldAI Extra: {protocol}://{host_name}:{port_str}{root_path}/api/extra"
14281469
) # noqa: E501
14291470

14301471
shutdown_task = await serve_http(

aphrodite/endpoints/openai/protocol.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,6 +1986,224 @@ class TranscriptionResponseVerbose(OpenAIBaseModel):
19861986
"""Extracted words and their corresponding timestamps."""
19871987

19881988

1989+
# ========== Anthropic Messages ========== #
1990+
1991+
# Anthropic Messages API Protocol Definitions
1992+
1993+
class AnthropicContentBlock(OpenAIBaseModel):
1994+
"""Base class for Anthropic content blocks"""
1995+
type: str
1996+
1997+
1998+
class AnthropicTextBlock(AnthropicContentBlock):
1999+
type: Literal["text"] = "text"
2000+
text: str
2001+
2002+
2003+
class AnthropicImageSource(OpenAIBaseModel):
2004+
type: Literal["base64"] = "base64"
2005+
media_type: Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
2006+
data: str
2007+
2008+
2009+
class AnthropicImageBlock(AnthropicContentBlock):
2010+
type: Literal["image"] = "image"
2011+
source: AnthropicImageSource
2012+
2013+
2014+
class AnthropicToolUseBlock(AnthropicContentBlock):
2015+
type: Literal["tool_use"] = "tool_use"
2016+
id: str
2017+
name: str
2018+
input: dict[str, Any]
2019+
2020+
2021+
class AnthropicToolResultBlock(AnthropicContentBlock):
2022+
type: Literal["tool_result"] = "tool_result"
2023+
tool_use_id: str
2024+
content: Optional[Union[str, list[Union[AnthropicTextBlock, AnthropicImageBlock]]]] = None
2025+
is_error: Optional[bool] = False
2026+
2027+
2028+
class AnthropicThinkingBlock(AnthropicContentBlock):
2029+
type: Literal["thinking"] = "thinking"
2030+
thinking: str
2031+
2032+
2033+
# Union type for all Anthropic content blocks
2034+
AnthropicContent = Union[
2035+
AnthropicTextBlock,
2036+
AnthropicImageBlock,
2037+
AnthropicToolUseBlock,
2038+
AnthropicToolResultBlock,
2039+
AnthropicThinkingBlock
2040+
]
2041+
2042+
2043+
class AnthropicMessage(OpenAIBaseModel):
2044+
role: Literal["user", "assistant"]
2045+
content: Union[str, list[AnthropicContent]]
2046+
2047+
2048+
class AnthropicTool(OpenAIBaseModel):
2049+
name: str
2050+
description: Optional[str] = None
2051+
input_schema: dict[str, Any]
2052+
2053+
2054+
class AnthropicToolChoiceAuto(OpenAIBaseModel):
2055+
type: Literal["auto"] = "auto"
2056+
disable_parallel_tool_use: Optional[bool] = False
2057+
2058+
2059+
class AnthropicToolChoiceAny(OpenAIBaseModel):
2060+
type: Literal["any"] = "any"
2061+
disable_parallel_tool_use: Optional[bool] = False
2062+
2063+
2064+
class AnthropicToolChoiceNone(OpenAIBaseModel):
2065+
type: Literal["none"] = "none"
2066+
2067+
2068+
class AnthropicToolChoiceTool(OpenAIBaseModel):
2069+
type: Literal["tool"] = "tool"
2070+
name: str
2071+
disable_parallel_tool_use: Optional[bool] = False
2072+
2073+
2074+
AnthropicToolChoice = Union[
2075+
AnthropicToolChoiceAuto,
2076+
AnthropicToolChoiceAny,
2077+
AnthropicToolChoiceNone,
2078+
AnthropicToolChoiceTool
2079+
]
2080+
2081+
2082+
class AnthropicThinkingConfig(OpenAIBaseModel):
2083+
"""Base class for thinking configurations"""
2084+
type: str
2085+
2086+
2087+
class AnthropicThinkingConfigEnabled(AnthropicThinkingConfig):
2088+
type: Literal["enabled"] = "enabled"
2089+
budget_tokens: int = Field(ge=1024)
2090+
2091+
2092+
class AnthropicThinkingConfigDisabled(AnthropicThinkingConfig):
2093+
type: Literal["disabled"] = "disabled"
2094+
2095+
2096+
AnthropicThinking = Union[AnthropicThinkingConfigEnabled, AnthropicThinkingConfigDisabled]
2097+
2098+
2099+
class AnthropicMetadata(OpenAIBaseModel):
2100+
user_id: Optional[str] = Field(None, max_length=256)
2101+
2102+
2103+
class AnthropicUsage(OpenAIBaseModel):
2104+
input_tokens: int
2105+
output_tokens: int
2106+
cache_creation_input_tokens: Optional[int] = None
2107+
cache_read_input_tokens: Optional[int] = None
2108+
2109+
2110+
class AnthropicMessagesRequest(OpenAIBaseModel):
2111+
model: str = Field(min_length=1, max_length=256)
2112+
messages: list[AnthropicMessage] = Field(max_items=100000)
2113+
max_tokens: int = Field(ge=1)
2114+
2115+
# Optional parameters
2116+
container: Optional[str] = None
2117+
metadata: Optional[AnthropicMetadata] = None
2118+
service_tier: Optional[Literal["auto", "standard_only"]] = None
2119+
stop_sequences: Optional[list[str]] = Field(default_factory=list)
2120+
stream: Optional[bool] = False
2121+
system: Optional[Union[str, list[AnthropicTextBlock]]] = None
2122+
temperature: Optional[float] = Field(None, ge=0.0, le=1.0)
2123+
thinking: Optional[AnthropicThinking] = None
2124+
tool_choice: Optional[AnthropicToolChoice] = None
2125+
tools: Optional[list[AnthropicTool]] = None
2126+
top_k: Optional[int] = Field(None, ge=0)
2127+
top_p: Optional[float] = Field(None, ge=0.0, le=1.0)
2128+
2129+
# Aphrodite-specific extensions for compatibility
2130+
request_id: Optional[str] = None
2131+
2132+
# For sampling params conversion
2133+
frequency_penalty: Optional[float] = 0.0
2134+
presence_penalty: Optional[float] = 0.0
2135+
seed: Optional[int] = None
2136+
2137+
# TODO: add more parameters
2138+
2139+
2140+
class AnthropicMessagesResponse(OpenAIBaseModel):
2141+
id: str
2142+
type: Literal["message"] = "message"
2143+
role: Literal["assistant"] = "assistant"
2144+
content: list[AnthropicContent]
2145+
model: str
2146+
stop_reason: Optional[Literal["end_turn", "max_tokens", "stop_sequence",
2147+
"tool_use", "pause_turn", "refusal"]]
2148+
stop_sequence: Optional[str] = None
2149+
usage: AnthropicUsage
2150+
container: Optional[dict[str, Any]] = None
2151+
2152+
2153+
# Streaming response classes
2154+
class AnthropicMessageStart(OpenAIBaseModel):
2155+
type: Literal["message_start"] = "message_start"
2156+
message: AnthropicMessagesResponse
2157+
2158+
2159+
class AnthropicContentBlockStart(OpenAIBaseModel):
2160+
type: Literal["content_block_start"] = "content_block_start"
2161+
index: int
2162+
content_block: AnthropicContent
2163+
2164+
2165+
class AnthropicContentBlockDelta(OpenAIBaseModel):
2166+
type: Literal["content_block_delta"] = "content_block_delta"
2167+
index: int
2168+
delta: dict[str, Any] # Contains the delta content
2169+
2170+
2171+
class AnthropicContentBlockStop(OpenAIBaseModel):
2172+
type: Literal["content_block_stop"] = "content_block_stop"
2173+
index: int
2174+
2175+
2176+
class AnthropicMessageDelta(OpenAIBaseModel):
2177+
type: Literal["message_delta"] = "message_delta"
2178+
delta: dict[str, Any]
2179+
usage: AnthropicUsage
2180+
2181+
2182+
class AnthropicMessageStop(OpenAIBaseModel):
2183+
type: Literal["message_stop"] = "message_stop"
2184+
2185+
2186+
class AnthropicPing(OpenAIBaseModel):
2187+
type: Literal["ping"] = "ping"
2188+
2189+
2190+
class AnthropicError(OpenAIBaseModel):
2191+
type: Literal["error"] = "error"
2192+
error: dict[str, Any]
2193+
2194+
2195+
AnthropicStreamEvent = Union[
2196+
AnthropicMessageStart,
2197+
AnthropicContentBlockStart,
2198+
AnthropicContentBlockDelta,
2199+
AnthropicContentBlockStop,
2200+
AnthropicMessageDelta,
2201+
AnthropicMessageStop,
2202+
AnthropicPing,
2203+
AnthropicError
2204+
]
2205+
2206+
19892207
# ========== KoboldAI ========== #
19902208

19912209

0 commit comments

Comments
 (0)