mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 14:14:57 +08:00
core/lmproto: add openai-compatible and deepseek clients
OpenAI-compatible models, config, and async client via niquests; DeepSeek OpenAI-compat extension with reasoning fields.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
from .openai_compat.client import DeepseekOpenAIClient as DeepseekOpenAIClient
|
||||||
|
from .openai_compat.model import AnyChatMessage as AnyChatMessage
|
||||||
|
from .openai_compat.model import AssistantChatMessage as AssistantChatMessage
|
||||||
|
from .openai_compat.model import ChatCompletionsParam as ChatCompletionsParam
|
||||||
|
from .openai_compat.model import DeepSeekReasoningEffort as DeepSeekReasoningEffort
|
||||||
|
from .openai_compat.model import NamedChatMessage as NamedChatMessage
|
||||||
|
from .openai_compat.model import ThinkingOptions as ThinkingOptions
|
||||||
|
from .openai_compat.model import ToolChatMessage as ToolChatMessage
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from typing import TYPE_CHECKING, Literal, overload
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
from ...openai_compatible.client import BaseOpenAIClient
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from ...openai_compatible.config import OpenAIConfig
|
||||||
|
from ...openai_compatible.model import ChatCompletionChunk, ChatCompletionResponse
|
||||||
|
from .model import ChatCompletionsParam
|
||||||
|
|
||||||
|
|
||||||
|
class DeepseekOpenAIClient(BaseOpenAIClient):
|
||||||
|
def __init__(self, config: OpenAIConfig) -> None:
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||||
|
) -> ChatCompletionResponse: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||||
|
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||||
|
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||||
|
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||||
|
param = msgspec.structs.replace(param, stream=stream)
|
||||||
|
data = self.encoder.encode(param)
|
||||||
|
if stream:
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
return self._parse_sse(resp)
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=False,
|
||||||
|
)
|
||||||
|
assert resp.content is not None
|
||||||
|
return self.decoder.decode(resp.content)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from msgspec import UNSET, Struct
|
||||||
|
|
||||||
|
from plyngent.typedef import Unset # noqa: TC001
|
||||||
|
|
||||||
|
from ...openai_compatible.model import AssistantChatMessage as BaseAssistantChatMessage
|
||||||
|
from ...openai_compatible.model import (
|
||||||
|
ChatMessage,
|
||||||
|
ReasoningEffort,
|
||||||
|
ResponseFormat,
|
||||||
|
StreamOptions,
|
||||||
|
ToolChoiceMode,
|
||||||
|
ToolFunctionItem,
|
||||||
|
)
|
||||||
|
from ...openai_compatible.model import ToolChatMessage as BaseToolChatMessage
|
||||||
|
|
||||||
|
type DeepSeekReasoningEffort = ReasoningEffort | Literal["max"]
|
||||||
|
|
||||||
|
|
||||||
|
class NamedChatMessage(ChatMessage):
|
||||||
|
role: Literal["system", "user"]
|
||||||
|
name: str | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantChatMessage(BaseAssistantChatMessage):
|
||||||
|
prefix: bool | Unset = UNSET
|
||||||
|
reasoning_content: str | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ToolChatMessage(BaseToolChatMessage):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
type AnyChatMessage = NamedChatMessage | AssistantChatMessage | ToolChatMessage
|
||||||
|
|
||||||
|
|
||||||
|
class ThinkingOptions(Struct):
|
||||||
|
type: Literal["enabled", "disabled"]
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionsParam(Struct):
|
||||||
|
messages: list[AnyChatMessage]
|
||||||
|
model: str
|
||||||
|
thinking: ThinkingOptions | Unset = UNSET
|
||||||
|
reasoning_effort: DeepSeekReasoningEffort | Unset = UNSET
|
||||||
|
max_tokens: int | Unset = UNSET
|
||||||
|
response_format: ResponseFormat | Unset = UNSET
|
||||||
|
stop: str | list[str] | Unset = UNSET
|
||||||
|
stream: bool | Unset = UNSET
|
||||||
|
stream_options: StreamOptions | Unset = UNSET
|
||||||
|
temperature: float | Unset = UNSET
|
||||||
|
top_p: int | Unset = UNSET
|
||||||
|
tool_choice: ToolChoiceMode | ToolFunctionItem | Unset = UNSET
|
||||||
|
tools: list[ToolFunctionItem] | Unset = UNSET
|
||||||
|
logprobs: bool | Unset = UNSET
|
||||||
|
top_logprobs: int | Unset = UNSET
|
||||||
|
user_id: str | Unset = UNSET
|
||||||
|
frequency_penalty: float | Unset = UNSET
|
||||||
|
presence_penalty: float | Unset = UNSET
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from .client import BaseOpenAIClient as BaseOpenAIClient
|
||||||
|
from .client import OpenAIClient as OpenAIClient
|
||||||
|
from .config import OpenAIConfig as OpenAIConfig
|
||||||
|
from .model import AnyAssistantToolCall as AnyAssistantToolCall
|
||||||
|
from .model import AnyChatMessage as AnyChatMessage
|
||||||
|
from .model import AnyResponseFormat as AnyResponseFormat
|
||||||
|
from .model import AnyToolItem as AnyToolItem
|
||||||
|
from .model import AssistantChatMessage as AssistantChatMessage
|
||||||
|
from .model import AudioFormatStr as AudioFormatStr
|
||||||
|
from .model import AudioOptions as AudioOptions
|
||||||
|
from .model import CacheRetention as CacheRetention
|
||||||
|
from .model import ChatCompletionChoice as ChatCompletionChoice
|
||||||
|
from .model import ChatCompletionChunk as ChatCompletionChunk
|
||||||
|
from .model import ChatCompletionResponse as ChatCompletionResponse
|
||||||
|
from .model import ChatCompletionsParam as ChatCompletionsParam
|
||||||
|
from .model import ChatMessage as ChatMessage
|
||||||
|
from .model import ChunkChoice as ChunkChoice
|
||||||
|
from .model import DeltaMessage as DeltaMessage
|
||||||
|
from .model import FinishReason as FinishReason
|
||||||
|
from .model import GrammarSyntax as GrammarSyntax
|
||||||
|
from .model import Modality as Modality
|
||||||
|
from .model import NamedChatMessage as NamedChatMessage
|
||||||
|
from .model import NamedRole as NamedRole
|
||||||
|
from .model import ReasoningEffort as ReasoningEffort
|
||||||
|
from .model import ResponseFormat as ResponseFormat
|
||||||
|
from .model import RoleAssistant as RoleAssistant
|
||||||
|
from .model import RoleTool as RoleTool
|
||||||
|
from .model import SchemaResponseFormat as SchemaResponseFormat
|
||||||
|
from .model import ServiceTier as ServiceTier
|
||||||
|
from .model import StreamOptions as StreamOptions
|
||||||
|
from .model import ToolChatMessage as ToolChatMessage
|
||||||
|
from .model import ToolChoiceMode as ToolChoiceMode
|
||||||
|
from .model import ToolFunctionItem as ToolFunctionItem
|
||||||
|
from .model import Verbosity as Verbosity
|
||||||
|
from .model import VoiceName as VoiceName
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from typing import TYPE_CHECKING, Literal, overload
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import niquests
|
||||||
|
from niquests.auth import BearerTokenAuth
|
||||||
|
|
||||||
|
from .config import OpenAIConfig # noqa: TC001
|
||||||
|
from .model import ChatCompletionChunk, ChatCompletionResponse, ChatCompletionsParam
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from niquests.async_session import AsyncSession
|
||||||
|
from niquests.models import AsyncResponse
|
||||||
|
|
||||||
|
|
||||||
|
class BaseOpenAIClient:
|
||||||
|
session: AsyncSession
|
||||||
|
encoder: msgspec.json.Encoder
|
||||||
|
decoder: msgspec.json.Decoder[ChatCompletionResponse]
|
||||||
|
chunk_decoder: msgspec.json.Decoder[ChatCompletionChunk]
|
||||||
|
|
||||||
|
def __init__(self, config: OpenAIConfig) -> None:
|
||||||
|
self.session = niquests.AsyncSession(
|
||||||
|
base_url=config.base_url,
|
||||||
|
auth=BearerTokenAuth(config.access_key_or_token),
|
||||||
|
)
|
||||||
|
self.encoder = msgspec.json.Encoder()
|
||||||
|
self.decoder = msgspec.json.Decoder(ChatCompletionResponse)
|
||||||
|
self.chunk_decoder = msgspec.json.Decoder(ChatCompletionChunk)
|
||||||
|
|
||||||
|
async def _parse_sse(self, resp: AsyncResponse) -> AsyncIterator[ChatCompletionChunk]:
|
||||||
|
lines = resp.iter_lines()
|
||||||
|
async for line in lines:
|
||||||
|
if not line or line == b"data: [DONE]":
|
||||||
|
continue
|
||||||
|
if line.startswith(b"data: "):
|
||||||
|
yield self.chunk_decoder.decode(line[6:])
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIClient(BaseOpenAIClient):
|
||||||
|
def __init__(self, config: OpenAIConfig) -> None:
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||||
|
) -> ChatCompletionResponse: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||||
|
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||||
|
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||||
|
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||||
|
param = msgspec.structs.replace(param, stream=stream)
|
||||||
|
data = self.encoder.encode(param)
|
||||||
|
if stream:
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
return self._parse_sse(resp)
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=False,
|
||||||
|
)
|
||||||
|
assert resp.content is not None
|
||||||
|
return self.decoder.decode(resp.content)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OpenAIConfig:
|
||||||
|
access_key_or_token: str
|
||||||
|
base_url: str = "https://api.openai.com/v1"
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from msgspec import UNSET, Struct
|
||||||
|
|
||||||
|
from plyngent.typedef import JSONSchema, Unset # noqa: TC001
|
||||||
|
|
||||||
|
type NamedRole = Literal["developer", "system", "user"]
|
||||||
|
type RoleAssistant = Literal["assistant"]
|
||||||
|
type RoleTool = Literal["tool"]
|
||||||
|
type ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
|
||||||
|
type ServiceTier = Literal["auto", "default", "flex", "scale", "priority"]
|
||||||
|
type ToolChoiceMode = Literal["none", "auto", "required"]
|
||||||
|
type AudioFormatStr = Literal["wav", "aac", "mp3", "flac", "opus", "pcm16"]
|
||||||
|
type VoiceName = Literal["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"]
|
||||||
|
type FinishReason = Literal["stop", "length", "tool_calls", "content_filter", "function_call"]
|
||||||
|
type Modality = Literal["text", "audio"]
|
||||||
|
type CacheRetention = Literal["in_memory", "24h"]
|
||||||
|
type Verbosity = Literal["low", "medium", "high"]
|
||||||
|
type GrammarSyntax = Literal["lark", "regex"]
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(Struct):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class NamedChatMessage(ChatMessage):
|
||||||
|
role: NamedRole
|
||||||
|
name: str | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class IDObject(Struct):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantFunctionTool(Struct):
|
||||||
|
name: str
|
||||||
|
arguments: str
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantFunctionToolCall(Struct):
|
||||||
|
id: str
|
||||||
|
type: Literal["function"]
|
||||||
|
function: AssistantFunctionTool
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantCustomTool(Struct):
|
||||||
|
name: str
|
||||||
|
input: str
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantCustomToolCall(Struct):
|
||||||
|
id: str
|
||||||
|
type: Literal["custom"]
|
||||||
|
custom: AssistantCustomTool
|
||||||
|
|
||||||
|
|
||||||
|
type AnyAssistantToolCall = AssistantFunctionToolCall | AssistantCustomToolCall
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantChatMessage(ChatMessage):
|
||||||
|
role: RoleAssistant
|
||||||
|
name: str | Unset = UNSET
|
||||||
|
audio: IDObject | Unset = UNSET
|
||||||
|
refusal: str | Unset = UNSET
|
||||||
|
tool_calls: list[AnyAssistantToolCall] | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ToolChatMessage(ChatMessage):
|
||||||
|
role: RoleTool
|
||||||
|
tool_call_id: str
|
||||||
|
|
||||||
|
|
||||||
|
type AnyChatMessage = NamedChatMessage | AssistantChatMessage | ToolChatMessage
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseFormat(Struct):
|
||||||
|
type: Literal["text", "json_object"]
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaResponseFormat(Struct):
|
||||||
|
type: Literal["json_schema"]
|
||||||
|
json_schema: JSONSchema
|
||||||
|
|
||||||
|
|
||||||
|
type AnyResponseFormat = ResponseFormat | SchemaResponseFormat
|
||||||
|
|
||||||
|
|
||||||
|
class ToolFunction(Struct):
|
||||||
|
name: str
|
||||||
|
description: str | Unset = UNSET
|
||||||
|
parameters: JSONSchema | Unset = UNSET
|
||||||
|
strict: bool | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ToolFunctionItem(Struct):
|
||||||
|
type: Literal["function"]
|
||||||
|
function: ToolFunction
|
||||||
|
|
||||||
|
|
||||||
|
class TextFormat(Struct):
|
||||||
|
type: Literal["text"]
|
||||||
|
|
||||||
|
|
||||||
|
class GrammarDefinition(Struct):
|
||||||
|
syntax: GrammarSyntax
|
||||||
|
definition: str
|
||||||
|
|
||||||
|
|
||||||
|
class GrammarFormat(Struct):
|
||||||
|
type: Literal["grammar"]
|
||||||
|
grammar: GrammarDefinition
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCustom(Struct):
|
||||||
|
name: str
|
||||||
|
description: str | Unset = UNSET
|
||||||
|
format: TextFormat | GrammarFormat | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCustomItem(Struct):
|
||||||
|
type: Literal["custom"]
|
||||||
|
custom: ToolCustom
|
||||||
|
|
||||||
|
|
||||||
|
type AnyToolItem = ToolFunctionItem | ToolCustomItem
|
||||||
|
|
||||||
|
|
||||||
|
class AudioOptions(Struct):
|
||||||
|
format: AudioFormatStr
|
||||||
|
voice: VoiceName | IDObject
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationOptions(Struct):
|
||||||
|
model: str
|
||||||
|
|
||||||
|
|
||||||
|
class PredictionOptions(Struct):
|
||||||
|
type: Literal["content"]
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class StreamOptions(Struct):
|
||||||
|
include_obfuscation: bool | Unset = UNSET
|
||||||
|
include_usage: bool | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedTools(Struct):
|
||||||
|
mode: Literal["auto", "required"]
|
||||||
|
tools: list[AnyToolItem]
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedToolChoice(Struct):
|
||||||
|
type: Literal["allowed_tools"]
|
||||||
|
allowed_tools: AllowedTools
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionsParam(Struct):
|
||||||
|
messages: list[AnyChatMessage]
|
||||||
|
model: str
|
||||||
|
audio: AudioOptions | Unset = UNSET
|
||||||
|
frequency_penalty: float | Unset = UNSET
|
||||||
|
logit_bias: dict[int, int] | Unset = UNSET
|
||||||
|
logprobs: bool | Unset = UNSET
|
||||||
|
max_completion_tokens: int | Unset = UNSET
|
||||||
|
max_tokens: int | Unset = UNSET
|
||||||
|
metadata: dict[str, str] | Unset = UNSET
|
||||||
|
modalities: set[Modality] | Unset = UNSET
|
||||||
|
moderation: ModerationOptions | Unset = UNSET
|
||||||
|
n: int | Unset = UNSET
|
||||||
|
parallel_tool_calls: bool | Unset = UNSET
|
||||||
|
prediction: PredictionOptions | Unset = UNSET
|
||||||
|
presence_penalty: float | Unset = UNSET
|
||||||
|
prompt_cache_key: str | Unset = UNSET
|
||||||
|
prompt_cache_retention: CacheRetention | Unset = UNSET
|
||||||
|
reasoning_effort: ReasoningEffort | Unset = UNSET
|
||||||
|
response_format: AnyResponseFormat | Unset = UNSET
|
||||||
|
safety_identifier: str | Unset = UNSET
|
||||||
|
seed: int | Unset = UNSET
|
||||||
|
service_tier: ServiceTier | Unset = UNSET
|
||||||
|
stop: str | list[str] | Unset = UNSET
|
||||||
|
store: bool | Unset = UNSET
|
||||||
|
stream: bool | Unset = UNSET
|
||||||
|
stream_options: StreamOptions | Unset = UNSET
|
||||||
|
temperature: float | Unset = UNSET
|
||||||
|
tool_choice: ToolChoiceMode | AllowedToolChoice | AnyToolItem | Unset = UNSET
|
||||||
|
tools: list[AnyToolItem] | Unset = UNSET
|
||||||
|
top_logprobs: int | Unset = UNSET
|
||||||
|
top_p: int | Unset = UNSET
|
||||||
|
user: str | Unset = UNSET
|
||||||
|
verbosity: Verbosity | Unset = UNSET
|
||||||
|
web_search_options: dict[str, Any] | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionChoice(Struct):
|
||||||
|
index: int
|
||||||
|
message: AssistantChatMessage
|
||||||
|
logprobs: dict[str, Any]
|
||||||
|
finish_reason: FinishReason
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionResponse(Struct):
|
||||||
|
id: str
|
||||||
|
object: Literal["chat.completion"]
|
||||||
|
created: int
|
||||||
|
model: str
|
||||||
|
choices: list[ChatCompletionChoice]
|
||||||
|
system_fingerprint: str
|
||||||
|
usage: dict[str, Any]
|
||||||
|
moderation: dict[str, Any] | Unset = UNSET
|
||||||
|
service_tier: ServiceTier | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class DeltaMessage(Struct):
|
||||||
|
role: RoleAssistant | Unset = UNSET
|
||||||
|
content: str | Unset = UNSET
|
||||||
|
tool_calls: list[AnyAssistantToolCall] | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkChoice(Struct):
|
||||||
|
index: int
|
||||||
|
delta: DeltaMessage
|
||||||
|
logprobs: dict[str, Any] | Unset = UNSET
|
||||||
|
finish_reason: FinishReason | None | Unset = UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionChunk(Struct):
|
||||||
|
id: str
|
||||||
|
object: Literal["chat.completion.chunk"]
|
||||||
|
created: int
|
||||||
|
model: str
|
||||||
|
choices: list[ChunkChoice]
|
||||||
|
usage: dict[str, Any] | Unset = UNSET
|
||||||
Reference in New Issue
Block a user