Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
498 lines
13 KiB
Python
498 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Integration tests for cross-provider message normalization.
|
|
|
|
Simulates a conversation that starts on one provider and is then formatted
|
|
for a *different* provider. The key invariant: provider-specific artefacts
|
|
from the first provider must not leak into the request payload for the
|
|
second provider, while the original in-memory messages must remain untouched.
|
|
"""
|
|
|
|
# pylint: disable=protected-access,redefined-outer-name
|
|
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from agentscope.formatter import OpenAIChatFormatter
|
|
from agentscope.message import (
|
|
Msg,
|
|
TextBlock,
|
|
ThinkingBlock,
|
|
ToolCallBlock,
|
|
ToolResultBlock,
|
|
)
|
|
|
|
try:
|
|
from agentscope.formatter import AnthropicChatFormatter
|
|
except ImportError:
|
|
AnthropicChatFormatter = None
|
|
|
|
try:
|
|
from agentscope.formatter import GeminiChatFormatter
|
|
except ImportError:
|
|
GeminiChatFormatter = None
|
|
|
|
from qwenpaw.agents import model_factory
|
|
|
|
|
|
def _gemini_session_history() -> list[Msg]:
|
|
"""Simulate a history built while Gemini was the active model."""
|
|
return [
|
|
Msg(
|
|
name="user",
|
|
role="user",
|
|
content=[TextBlock(text="Find the weather in Tokyo")],
|
|
),
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
ToolCallBlock(
|
|
type="tool_call",
|
|
id="tc_gemini_1",
|
|
name="get_weather",
|
|
input=json.dumps({"city": "Tokyo"}),
|
|
),
|
|
ToolResultBlock(
|
|
type="tool_result",
|
|
id="tc_gemini_1",
|
|
name="get_weather",
|
|
output="Sunny, 25°C",
|
|
),
|
|
],
|
|
),
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
TextBlock(text="The weather in Tokyo is sunny and 25°C."),
|
|
],
|
|
),
|
|
]
|
|
|
|
|
|
def _openai_session_history() -> list[Msg]:
|
|
"""Simulate a plain history with no provider-specific artefacts."""
|
|
return [
|
|
Msg(
|
|
name="user",
|
|
role="user",
|
|
content=[TextBlock(text="Say hello")],
|
|
),
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[TextBlock(text="Hello!")],
|
|
),
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gemini → OpenAI switch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gemini_history_to_openai(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
history = _gemini_session_history()
|
|
original_dict = history[1].to_dict()
|
|
|
|
(
|
|
normalized,
|
|
is_anthropic,
|
|
is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
history,
|
|
OpenAIChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
assert is_anthropic is False
|
|
assert is_gemini is False
|
|
|
|
tool_call_block = normalized[1].content[0]
|
|
assert tool_call_block.type == "tool_call"
|
|
assert tool_call_block.id == "tc_gemini_1"
|
|
|
|
assert history[1].to_dict() == original_dict
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gemini → Anthropic switch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gemini_history_to_anthropic(monkeypatch) -> None:
|
|
if AnthropicChatFormatter is None:
|
|
pytest.skip("AnthropicChatFormatter not available")
|
|
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
history = _gemini_session_history()
|
|
|
|
(
|
|
_,
|
|
is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
history,
|
|
AnthropicChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
assert is_anthropic is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gemini → Gemini (same provider, no stripping)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gemini_history_stays_gemini(monkeypatch) -> None:
|
|
if GeminiChatFormatter is None:
|
|
pytest.skip("GeminiChatFormatter not available")
|
|
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
history = _gemini_session_history()
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
history,
|
|
GeminiChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
assert is_gemini is True
|
|
block = normalized[1].content[0]
|
|
assert block.type == "tool_call"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OpenAI → Gemini (nothing to strip, no crash)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_openai_history_to_gemini(monkeypatch) -> None:
|
|
if GeminiChatFormatter is None:
|
|
pytest.skip("GeminiChatFormatter not available")
|
|
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
history = _openai_session_history()
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
history,
|
|
GeminiChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
assert is_gemini is True
|
|
assert normalized[0].content[0].text == "Say hello"
|
|
assert normalized[1].content[0].text == "Hello!"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multiple tool calls in one message
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gemini_multi_toolcall_to_openai(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
msgs = [
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
ToolCallBlock(
|
|
type="tool_call",
|
|
id="tc_a",
|
|
name="fn_a",
|
|
input="{}",
|
|
),
|
|
ToolCallBlock(
|
|
type="tool_call",
|
|
id="tc_b",
|
|
name="fn_b",
|
|
input="{}",
|
|
),
|
|
ToolResultBlock(
|
|
type="tool_result",
|
|
id="tc_a",
|
|
name="fn_a",
|
|
output="ok_a",
|
|
),
|
|
ToolResultBlock(
|
|
type="tool_result",
|
|
id="tc_b",
|
|
name="fn_b",
|
|
output="ok_b",
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
msgs,
|
|
OpenAIChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
for block in normalized[0].content:
|
|
if getattr(block, "type", None) == "tool_call":
|
|
assert not hasattr(block, "extra_content") or not getattr(
|
|
block,
|
|
"extra_content",
|
|
None,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thinking blocks cross-provider
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _history_with_thinking() -> list[Msg]:
|
|
return [
|
|
Msg(
|
|
name="user",
|
|
role="user",
|
|
content=[TextBlock(text="Think about this")],
|
|
),
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
ThinkingBlock(thinking="Let me consider..."),
|
|
TextBlock(text="Here is my answer."),
|
|
],
|
|
),
|
|
]
|
|
|
|
|
|
def test_thinking_blocks_preserved_for_openai(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
_history_with_thinking(),
|
|
OpenAIChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
blocks = normalized[1].content
|
|
thinking_blocks = [
|
|
b for b in blocks if getattr(b, "type", None) == "thinking"
|
|
]
|
|
assert len(thinking_blocks) == 1
|
|
assert thinking_blocks[0].thinking == "Let me consider..."
|
|
|
|
|
|
def test_unsigned_thinking_blocks_dropped_for_anthropic(monkeypatch) -> None:
|
|
if AnthropicChatFormatter is None:
|
|
pytest.skip("AnthropicChatFormatter not available")
|
|
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
_history_with_thinking(),
|
|
AnthropicChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
blocks = normalized[1].content
|
|
thinking_blocks = [
|
|
b for b in blocks if getattr(b, "type", None) == "thinking"
|
|
]
|
|
assert thinking_blocks == []
|
|
text_blocks = [b for b in blocks if getattr(b, "type", None) == "text"]
|
|
assert len(text_blocks) == 1
|
|
|
|
|
|
def test_signed_thinking_blocks_preserved_for_anthropic(monkeypatch) -> None:
|
|
if AnthropicChatFormatter is None:
|
|
pytest.skip("AnthropicChatFormatter not available")
|
|
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
history = [
|
|
Msg(
|
|
name="user",
|
|
role="user",
|
|
content=[TextBlock(text="Think about this")],
|
|
),
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
ThinkingBlock(
|
|
thinking="Let me consider...",
|
|
signature="sig-from-claude",
|
|
),
|
|
TextBlock(text="Here is my answer."),
|
|
],
|
|
),
|
|
]
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
history,
|
|
AnthropicChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
blocks = normalized[1].content
|
|
thinking_blocks = [
|
|
b for b in blocks if getattr(b, "type", None) == "thinking"
|
|
]
|
|
assert len(thinking_blocks) == 1
|
|
assert thinking_blocks[0].signature == "sig-from-claude"
|
|
|
|
|
|
def test_thinking_blocks_preserved_for_gemini(monkeypatch) -> None:
|
|
if GeminiChatFormatter is None:
|
|
pytest.skip("GeminiChatFormatter not available")
|
|
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
_history_with_thinking(),
|
|
GeminiChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
blocks = normalized[1].content
|
|
thinking_blocks = [
|
|
b for b in blocks if getattr(b, "type", None) == "thinking"
|
|
]
|
|
assert len(thinking_blocks) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# raw_input repair survives across provider switches
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _history_with_raw_input_needing_repair() -> list[Msg]:
|
|
return [
|
|
Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
ToolCallBlock(
|
|
type="tool_call",
|
|
id="tc_repair",
|
|
name="search",
|
|
input="{}",
|
|
),
|
|
ToolResultBlock(
|
|
type="tool_result",
|
|
id="tc_repair",
|
|
name="search",
|
|
output="found it",
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
|
|
def test_raw_input_repair_works_before_cross_provider_clean(
|
|
monkeypatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
model_factory,
|
|
"_supports_multimodal_for_current_model",
|
|
lambda: True,
|
|
)
|
|
|
|
history = _history_with_raw_input_needing_repair()
|
|
|
|
(
|
|
normalized,
|
|
_is_anthropic,
|
|
_is_gemini,
|
|
_is_response,
|
|
) = model_factory._normalize_messages_for_formatter(
|
|
history,
|
|
OpenAIChatFormatter,
|
|
SimpleNamespace(),
|
|
)
|
|
|
|
block = normalized[0].content[0]
|
|
assert not hasattr(block, "raw_input") or not getattr(
|
|
block,
|
|
"raw_input",
|
|
None,
|
|
)
|