mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
lmproto/openai_compatible: drop provider blocks on a chat wire
`provider_blocks` means something only to the surface that produced them (Anthropic `thinking` / server-tool blocks). A session that switched surface would otherwise send those dicts to a chat-completions provider as an assistant-message key it does not know; the request normalization drops the field, on a copy, so the history keeps it.
This commit is contained in:
@@ -5,8 +5,9 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
from msgspec import UNSET
|
||||||
|
|
||||||
from .model import DeveloperChatMessage, SystemChatMessage
|
from .model import AssistantChatMessage, DeveloperChatMessage, SystemChatMessage
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
@@ -37,9 +38,31 @@ def coerce_developer_messages_to_system(messages: Sequence[AnyChatMessage]) -> l
|
|||||||
return out if changed else list(messages)
|
return out if changed else list(messages)
|
||||||
|
|
||||||
|
|
||||||
|
def coerce_chat_messages(messages: Sequence[AnyChatMessage]) -> list[AnyChatMessage]:
|
||||||
|
"""Return *messages* as a chat-completions wire can take them.
|
||||||
|
|
||||||
|
Both normalizations are for broad provider compatibility: a ``developer``
|
||||||
|
role becomes ``system`` (see :func:`coerce_developer_messages_to_system`),
|
||||||
|
and ``AssistantChatMessage.provider_blocks`` is dropped — those blocks
|
||||||
|
belong to the surface that produced them (Anthropic ``thinking`` and
|
||||||
|
server-tool blocks; see ``agent.messages_bridge``) and no other provider
|
||||||
|
knows the key.
|
||||||
|
"""
|
||||||
|
coerced = coerce_developer_messages_to_system(messages)
|
||||||
|
changed = False
|
||||||
|
out: list[AnyChatMessage] = []
|
||||||
|
for message in coerced:
|
||||||
|
if isinstance(message, AssistantChatMessage) and message.provider_blocks is not UNSET:
|
||||||
|
out.append(msgspec.structs.replace(message, provider_blocks=UNSET))
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
out.append(message)
|
||||||
|
return out if changed else coerced
|
||||||
|
|
||||||
|
|
||||||
def coerce_chat_completions_param(param: ChatCompletionsParam) -> ChatCompletionsParam:
|
def coerce_chat_completions_param(param: ChatCompletionsParam) -> ChatCompletionsParam:
|
||||||
"""Normalize a chat-completions request for broad provider compatibility."""
|
"""Normalize a chat-completions request for broad provider compatibility."""
|
||||||
messages = coerce_developer_messages_to_system(param.messages)
|
messages = coerce_chat_messages(param.messages)
|
||||||
if messages == param.messages:
|
if messages == param.messages:
|
||||||
return param
|
return param
|
||||||
return msgspec.structs.replace(param, messages=messages)
|
return msgspec.structs.replace(param, messages=messages)
|
||||||
@@ -56,7 +79,7 @@ def coerce_chat_completions_param_any(param: Any) -> Any:
|
|||||||
if not isinstance(messages_obj, list):
|
if not isinstance(messages_obj, list):
|
||||||
return param
|
return param
|
||||||
messages = cast("list[AnyChatMessage]", messages_obj)
|
messages = cast("list[AnyChatMessage]", messages_obj)
|
||||||
coerced = coerce_developer_messages_to_system(messages)
|
coerced = coerce_chat_messages(messages)
|
||||||
if coerced == messages:
|
if coerced == messages:
|
||||||
return param
|
return param
|
||||||
return msgspec.structs.replace(param, messages=coerced)
|
return msgspec.structs.replace(param, messages=coerced)
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ from __future__ import annotations
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
from msgspec import UNSET
|
||||||
|
|
||||||
from plyngent.lmproto.deepseek.openai_compat.model import ChatCompletionsParam as DeepseekChatCompletionsParam
|
from plyngent.lmproto.deepseek.openai_compat.model import ChatCompletionsParam as DeepseekChatCompletionsParam
|
||||||
from plyngent.lmproto.openai_compatible.compat import (
|
from plyngent.lmproto.openai_compatible.compat import (
|
||||||
coerce_chat_completions_param,
|
coerce_chat_completions_param,
|
||||||
coerce_chat_completions_param_any,
|
coerce_chat_completions_param_any,
|
||||||
|
coerce_chat_messages,
|
||||||
coerce_developer_messages_to_system,
|
coerce_developer_messages_to_system,
|
||||||
developer_to_system_message,
|
developer_to_system_message,
|
||||||
)
|
)
|
||||||
@@ -88,3 +90,32 @@ def test_generic_coercion_handles_deepseek_runtime_param() -> None:
|
|||||||
coerced = coerce_chat_completions_param_any(base)
|
coerced = coerce_chat_completions_param_any(base)
|
||||||
raw = msgspec.json.decode(msgspec.json.encode(coerced))
|
raw = msgspec.json.decode(msgspec.json.encode(coerced))
|
||||||
assert raw["messages"][0]["role"] == "system"
|
assert raw["messages"][0]["role"] == "system"
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce_chat_messages_drops_provider_blocks() -> None:
|
||||||
|
"""Another surface's opaque blocks are not a chat-completions key."""
|
||||||
|
messages: list[AnyChatMessage] = [
|
||||||
|
UserChatMessage(content="hi"),
|
||||||
|
AssistantChatMessage(
|
||||||
|
content="done",
|
||||||
|
reasoning_content="hmm",
|
||||||
|
provider_blocks=[{"type": "thinking", "thinking": "hmm", "signature": "sig-1"}],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
coerced = coerce_chat_messages(messages)
|
||||||
|
assistant = coerced[1]
|
||||||
|
assert isinstance(assistant, AssistantChatMessage)
|
||||||
|
assert assistant.provider_blocks is UNSET
|
||||||
|
# The reasoning slot is the provider-neutral one and stays.
|
||||||
|
assert assistant.reasoning_content == "hmm"
|
||||||
|
raw = msgspec.json.decode(msgspec.json.encode(ChatCompletionsParam(model="m", messages=coerced)))
|
||||||
|
assert "provider_blocks" not in raw["messages"][1]
|
||||||
|
# The history the copy came from keeps them for the surface that made them.
|
||||||
|
original = messages[1]
|
||||||
|
assert isinstance(original, AssistantChatMessage)
|
||||||
|
assert original.provider_blocks is not UNSET
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce_chat_messages_leaves_plain_history_alone() -> None:
|
||||||
|
messages: list[AnyChatMessage] = [UserChatMessage(content="hi"), AssistantChatMessage(content="done")]
|
||||||
|
assert coerce_chat_messages(messages) == messages
|
||||||
|
|||||||
Reference in New Issue
Block a user