mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/lmproto: coerce developer role for chat completions
This commit is contained in:
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Literal, overload
|
||||
import msgspec
|
||||
|
||||
from ...openai_compatible.client import BaseOpenAIClient, read_response_body
|
||||
from ...openai_compatible.compat import coerce_chat_completions_param_any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -29,7 +30,7 @@ class DeepseekOpenAIClient(BaseOpenAIClient):
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||
param = msgspec.structs.replace(param, stream=stream)
|
||||
param = coerce_chat_completions_param_any(msgspec.structs.replace(param, stream=stream))
|
||||
data = self.encoder.encode(param)
|
||||
if stream:
|
||||
resp = await self.session.post(
|
||||
|
||||
@@ -8,6 +8,7 @@ import niquests
|
||||
from msgspec import UNSET
|
||||
from niquests.auth import BearerTokenAuth
|
||||
|
||||
from .compat import coerce_chat_completions_param
|
||||
from .config import OpenAIConfig # noqa: TC001
|
||||
from .model import (
|
||||
AssistantFunctionTool,
|
||||
@@ -196,7 +197,7 @@ class OpenAICompatibleClient(BaseOpenAIClient):
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||
# Library pattern: async def returns AsyncIterator when stream=True.
|
||||
param = msgspec.structs.replace(param, stream=stream)
|
||||
param = coerce_chat_completions_param(msgspec.structs.replace(param, stream=stream))
|
||||
data = self.encoder.encode(param)
|
||||
if stream:
|
||||
resp = await self.session.post(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Message normalization for chat-completions-compatible wire APIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import msgspec
|
||||
|
||||
from .model import DeveloperChatMessage, SystemChatMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from .model import AnyChatMessage, ChatCompletionsParam
|
||||
|
||||
|
||||
def developer_to_system_message(message: DeveloperChatMessage) -> SystemChatMessage:
|
||||
"""Convert an internal developer-role message to classic chat ``system`` role."""
|
||||
return SystemChatMessage(content=message.content, name=message.name)
|
||||
|
||||
|
||||
def coerce_developer_messages_to_system(messages: Sequence[AnyChatMessage]) -> list[AnyChatMessage]:
|
||||
"""Return *messages* with ``developer`` roles rewritten as ``system``.
|
||||
|
||||
Most OpenAI-compatible ``/chat/completions`` providers only accept
|
||||
system/user/assistant/tool. Internal developer checkpoints still carry
|
||||
control-plane intent best as ``system`` on that wire protocol.
|
||||
"""
|
||||
changed = False
|
||||
out: list[AnyChatMessage] = []
|
||||
for message in messages:
|
||||
if isinstance(message, DeveloperChatMessage):
|
||||
out.append(developer_to_system_message(message))
|
||||
changed = True
|
||||
else:
|
||||
out.append(message)
|
||||
return out if changed else list(messages)
|
||||
|
||||
|
||||
def coerce_chat_completions_param(param: ChatCompletionsParam) -> ChatCompletionsParam:
|
||||
"""Normalize a chat-completions request for broad provider compatibility."""
|
||||
messages = coerce_developer_messages_to_system(param.messages)
|
||||
if messages == param.messages:
|
||||
return param
|
||||
return msgspec.structs.replace(param, messages=messages)
|
||||
|
||||
|
||||
def coerce_chat_completions_param_any(param: Any) -> Any:
|
||||
"""Generic variant for provider-specific chat param structs.
|
||||
|
||||
DeepSeek's request type excludes ``DeveloperChatMessage`` from its annotation,
|
||||
but callers can still pass the generic agent param shape at runtime. Replace
|
||||
messages structurally before encoding without depending on the exact class.
|
||||
"""
|
||||
messages_obj: Any = getattr(param, "messages", None)
|
||||
if not isinstance(messages_obj, list):
|
||||
return param
|
||||
messages = cast("list[AnyChatMessage]", messages_obj)
|
||||
coerced = coerce_developer_messages_to_system(messages)
|
||||
if coerced == messages:
|
||||
return param
|
||||
return msgspec.structs.replace(param, messages=coerced)
|
||||
Reference in New Issue
Block a user