mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/lmproto: add Anthropic messages models, config, and client
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from .client import AnthropicClient as AnthropicClient
|
||||
from .config import AnthropicConfig as AnthropicConfig
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Anthropic ``/messages`` HTTP client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
||||
|
||||
import msgspec
|
||||
import niquests
|
||||
|
||||
from .model import (
|
||||
AnthropicErrorEvent,
|
||||
AnthropicMessageResponse,
|
||||
AnthropicMessagesParam,
|
||||
AnthropicMessageStop,
|
||||
AnthropicModelsResponse,
|
||||
AnthropicStreamEvent,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from niquests.async_session import AsyncSession
|
||||
|
||||
from .config import AnthropicConfig
|
||||
|
||||
|
||||
class AnthropicClient:
|
||||
"""Anthropic API client for ``POST /messages`` + ``GET /models``."""
|
||||
|
||||
kind: str = "messages"
|
||||
_HTTP_ERROR: int = 400
|
||||
session: AsyncSession
|
||||
encoder: msgspec.json.Encoder
|
||||
decoder: msgspec.json.Decoder[AnthropicMessageResponse]
|
||||
stream_decoder: msgspec.json.Decoder[AnthropicStreamEvent]
|
||||
models_decoder: msgspec.json.Decoder[AnthropicModelsResponse]
|
||||
_api_key: str
|
||||
_api_version: str
|
||||
|
||||
def __init__(self, config: AnthropicConfig) -> None:
|
||||
self.session = niquests.AsyncSession(base_url=config.base_url, timeout=config.timeout)
|
||||
self.encoder = msgspec.json.Encoder()
|
||||
self.decoder = msgspec.json.Decoder(AnthropicMessageResponse)
|
||||
self.stream_decoder = msgspec.json.Decoder(AnthropicStreamEvent)
|
||||
self.models_decoder = msgspec.json.Decoder(AnthropicModelsResponse)
|
||||
self._api_key = config.api_key
|
||||
self._api_version = config.anthropic_version
|
||||
|
||||
def _headers(self) -> dict[str, str]:
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": self._api_key,
|
||||
"anthropic-version": self._api_version,
|
||||
}
|
||||
|
||||
async def models(self) -> list[str]:
|
||||
resp = await self.session.get("/models", headers=self._headers(), stream=False)
|
||||
body = await self._read_json_body(resp, what="models")
|
||||
parsed = self.models_decoder.decode(body)
|
||||
return [item.id for item in parsed.data if item.id]
|
||||
|
||||
@overload
|
||||
async def messages(
|
||||
self, param: AnthropicMessagesParam, *, stream: Literal[False] = False
|
||||
) -> AnthropicMessageResponse: ...
|
||||
|
||||
@overload
|
||||
async def messages(
|
||||
self, param: AnthropicMessagesParam, *, stream: Literal[True]
|
||||
) -> AsyncIterator[AnthropicStreamEvent]: ...
|
||||
|
||||
async def messages(
|
||||
self, param: AnthropicMessagesParam, *, stream: bool = False
|
||||
) -> AnthropicMessageResponse | AsyncIterator[AnthropicStreamEvent]:
|
||||
param = msgspec.structs.replace(param, stream=stream)
|
||||
data = self.encoder.encode(param)
|
||||
if stream:
|
||||
resp = await self.session.post("/messages", data=data, headers=self._headers(), stream=True)
|
||||
return self._parse_sse(resp)
|
||||
resp = await self.session.post("/messages", data=data, headers=self._headers(), stream=False)
|
||||
body = await self._read_json_body(resp, what="messages")
|
||||
return self.decoder.decode(body)
|
||||
|
||||
async def _ensure_ok(self, resp: object, *, what: str = "request") -> None:
|
||||
from plyngent.lmproto.openai_compatible.client import http_error_message, read_response_body
|
||||
|
||||
status = getattr(resp, "status_code", None)
|
||||
if not isinstance(status, int):
|
||||
return
|
||||
body: bytes | str | None = None
|
||||
if status >= AnthropicClient._HTTP_ERROR:
|
||||
body = await read_response_body(resp)
|
||||
msg = http_error_message(status, body, what=what)
|
||||
if msg is None:
|
||||
return
|
||||
raise RuntimeError(msg)
|
||||
|
||||
async def _read_json_body(self, resp: object, *, what: str) -> bytes:
|
||||
from plyngent.lmproto.openai_compatible.client import read_response_body
|
||||
|
||||
await self._ensure_ok(resp, what=what)
|
||||
body = await read_response_body(resp)
|
||||
if body is None:
|
||||
msg = f"{what} response body is empty"
|
||||
raise RuntimeError(msg)
|
||||
if not isinstance(body, (bytes, bytearray)):
|
||||
msg = f"{what} response body has unexpected type {type(body)!r}"
|
||||
raise TypeError(msg)
|
||||
return bytes(body)
|
||||
|
||||
async def _parse_sse(self, resp: object) -> AsyncIterator[AnthropicStreamEvent]:
|
||||
from plyngent.lmproto.openai_compatible.client import close_async_response, sse_data_payload
|
||||
|
||||
typed = cast("Any", resp)
|
||||
try:
|
||||
await self._ensure_ok(typed, what="messages")
|
||||
async for raw in typed.iter_lines():
|
||||
if not raw:
|
||||
continue
|
||||
parsed = sse_data_payload(bytes(raw))
|
||||
if parsed is None:
|
||||
continue
|
||||
if parsed is False:
|
||||
break
|
||||
event = self.stream_decoder.decode(parsed)
|
||||
yield event
|
||||
if isinstance(event, (AnthropicMessageStop, AnthropicErrorEvent)):
|
||||
break
|
||||
finally:
|
||||
await close_async_response(typed)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Anthropic API configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from plyngent.lmproto.openai_compatible.config import (
|
||||
DEFAULT_HTTP_CONNECT_TIMEOUT,
|
||||
DEFAULT_HTTP_READ_TIMEOUT,
|
||||
HttpTimeout,
|
||||
)
|
||||
|
||||
DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1"
|
||||
DEFAULT_ANTHROPIC_VERSION = "2023-06-01"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnthropicConfig:
|
||||
api_key: str
|
||||
base_url: str = DEFAULT_ANTHROPIC_BASE_URL
|
||||
anthropic_version: str = DEFAULT_ANTHROPIC_VERSION
|
||||
timeout: HttpTimeout = (DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""Anthropic ``/messages`` API msgspec models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from msgspec import UNSET, Struct, field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from plyngent.typedef import Unset
|
||||
|
||||
|
||||
class AnthropicTextContent(Struct, tag_field="type", tag="text"):
|
||||
text: str
|
||||
|
||||
|
||||
class AnthropicImageContent(Struct, tag_field="type", tag="image"):
|
||||
source: dict[str, Any]
|
||||
|
||||
|
||||
class AnthropicToolUseContent(Struct, tag_field="type", tag="tool_use"):
|
||||
id: str
|
||||
name: str
|
||||
input: dict[str, Any]
|
||||
|
||||
|
||||
class AnthropicToolResultContent(Struct, tag_field="type", tag="tool_result"):
|
||||
tool_use_id: str
|
||||
content: str | list[AnthropicTextContent | AnthropicImageContent]
|
||||
is_error: bool = False
|
||||
|
||||
|
||||
type AnthropicContentBlock = (
|
||||
AnthropicTextContent | AnthropicImageContent | AnthropicToolUseContent | AnthropicToolResultContent
|
||||
)
|
||||
|
||||
|
||||
class AnthropicUserMessage(Struct, tag_field="role", tag="user"):
|
||||
content: str | list[AnthropicTextContent | AnthropicImageContent | AnthropicToolResultContent]
|
||||
|
||||
|
||||
class AnthropicAssistantMessage(Struct, tag_field="role", tag="assistant"):
|
||||
content: str | list[AnthropicTextContent | AnthropicToolUseContent]
|
||||
|
||||
|
||||
type AnthropicMessage = AnthropicUserMessage | AnthropicAssistantMessage
|
||||
|
||||
|
||||
class AnthropicToolDefinition(Struct, omit_defaults=True):
|
||||
name: str
|
||||
description: str | Unset = UNSET
|
||||
input_schema: dict[str, Any] | Unset = UNSET
|
||||
|
||||
|
||||
class AnthropicToolChoice(Struct, omit_defaults=True):
|
||||
type: Literal["auto", "any", "tool"] = "auto"
|
||||
name: str | Unset = UNSET
|
||||
disable_parallel_tool_use: bool | Unset = UNSET
|
||||
|
||||
|
||||
class AnthropicMetadata(Struct, omit_defaults=True):
|
||||
user_id: str | Unset = UNSET
|
||||
|
||||
|
||||
class AnthropicMessagesParam(Struct, omit_defaults=True):
|
||||
model: str
|
||||
max_tokens: int = 8192
|
||||
messages: list[AnthropicMessage] = field(default_factory=list)
|
||||
system: str | list[AnthropicTextContent] | Unset = UNSET
|
||||
tools: list[AnthropicToolDefinition] | Unset = UNSET
|
||||
tool_choice: AnthropicToolChoice | Unset = UNSET
|
||||
metadata: AnthropicMetadata | Unset = UNSET
|
||||
stop_sequences: list[str] | Unset = UNSET
|
||||
stream: bool = False
|
||||
temperature: float | Unset = UNSET
|
||||
top_p: float | Unset = UNSET
|
||||
top_k: int | Unset = UNSET
|
||||
|
||||
|
||||
class AnthropicUsage(Struct, omit_defaults=True):
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cache_creation_input_tokens: int | None = None
|
||||
cache_read_input_tokens: int | None = None
|
||||
|
||||
|
||||
class AnthropicResponseText(Struct, tag_field="type", tag="text"):
|
||||
text: str
|
||||
|
||||
|
||||
class AnthropicResponseToolUse(Struct, tag_field="type", tag="tool_use"):
|
||||
id: str
|
||||
name: str
|
||||
input: dict[str, Any]
|
||||
|
||||
|
||||
type AnthropicResponseContent = AnthropicResponseText | AnthropicResponseToolUse
|
||||
|
||||
|
||||
class AnthropicMessageResponse(Struct, omit_defaults=True):
|
||||
id: str
|
||||
type: str = "message"
|
||||
role: str = "assistant"
|
||||
content: list[AnthropicResponseContent] = field(default_factory=list)
|
||||
model: str = ""
|
||||
stop_reason: str | None = None
|
||||
stop_sequence: str | None = None
|
||||
usage: AnthropicUsage = field(default_factory=AnthropicUsage)
|
||||
|
||||
|
||||
class AnthropicRawContentBlock(Struct, omit_defaults=True):
|
||||
type: str = ""
|
||||
id: str | None = None
|
||||
name: str | None = None
|
||||
text: str | None = None
|
||||
input: dict[str, Any] | None = None
|
||||
partial_json: str | None = None
|
||||
|
||||
|
||||
class AnthropicMessageStart(Struct, tag_field="type", tag="message_start"):
|
||||
message: AnthropicMessageResponse
|
||||
|
||||
|
||||
class AnthropicPing(Struct, tag_field="type", tag="ping"):
|
||||
pass
|
||||
|
||||
|
||||
class AnthropicContentBlockStart(Struct, tag_field="type", tag="content_block_start"):
|
||||
index: int
|
||||
content_block: AnthropicRawContentBlock
|
||||
|
||||
|
||||
class AnthropicContentBlockDelta(Struct, tag_field="type", tag="content_block_delta"):
|
||||
index: int
|
||||
delta: AnthropicRawContentBlock
|
||||
|
||||
|
||||
class AnthropicContentBlockStop(Struct, tag_field="type", tag="content_block_stop"):
|
||||
index: int
|
||||
|
||||
|
||||
class AnthropicMessageDelta(Struct, tag_field="type", tag="message_delta"):
|
||||
delta: dict[str, Any]
|
||||
usage: AnthropicUsage
|
||||
|
||||
|
||||
class AnthropicMessageStop(Struct, tag_field="type", tag="message_stop"):
|
||||
pass
|
||||
|
||||
|
||||
class AnthropicErrorEvent(Struct, tag_field="type", tag="error"):
|
||||
error: dict[str, Any]
|
||||
|
||||
|
||||
type AnthropicStreamEvent = (
|
||||
AnthropicMessageStart
|
||||
| AnthropicPing
|
||||
| AnthropicContentBlockStart
|
||||
| AnthropicContentBlockDelta
|
||||
| AnthropicContentBlockStop
|
||||
| AnthropicMessageDelta
|
||||
| AnthropicMessageStop
|
||||
| AnthropicErrorEvent
|
||||
)
|
||||
|
||||
|
||||
class AnthropicModelInfo(Struct, omit_defaults=True):
|
||||
type: str = ""
|
||||
id: str = ""
|
||||
|
||||
|
||||
class AnthropicModelsResponse(Struct, omit_defaults=True):
|
||||
data: list[AnthropicModelInfo] = field(default_factory=list)
|
||||
Reference in New Issue
Block a user