mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-09 00:47:54 +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