mirror of
https://github.com/mistralai/mistral-vibe
synced 2026-04-25 17:14:55 +02:00
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Peter Evers <pevers90@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@protonmail.com> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
255 lines
8.6 KiB
Python
255 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from vibe.core.config import ProviderConfig
|
|
from vibe.core.llm.backend.reasoning_adapter import ReasoningAdapter
|
|
from vibe.core.types import (
|
|
AvailableFunction,
|
|
AvailableTool,
|
|
FunctionCall,
|
|
LLMMessage,
|
|
Role,
|
|
ToolCall,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def adapter():
|
|
return ReasoningAdapter()
|
|
|
|
|
|
@pytest.fixture
|
|
def provider():
|
|
return ProviderConfig(
|
|
name="test-reasoning",
|
|
api_base="https://api.example.com/v1",
|
|
api_key_env_var="TEST_API_KEY",
|
|
api_style="reasoning",
|
|
)
|
|
|
|
|
|
def _prepare(adapter, provider, messages, **kwargs):
|
|
defaults = dict(
|
|
model_name="m",
|
|
messages=messages,
|
|
temperature=0,
|
|
tools=None,
|
|
max_tokens=None,
|
|
tool_choice=None,
|
|
enable_streaming=False,
|
|
provider=provider,
|
|
)
|
|
defaults.update(kwargs)
|
|
return json.loads(adapter.prepare_request(**defaults).body)
|
|
|
|
|
|
class TestReasoningEffort:
|
|
@pytest.mark.parametrize("level", ["low", "medium", "high"])
|
|
def test_sets_reasoning_effort(self, adapter, provider, level):
|
|
payload = _prepare(
|
|
adapter,
|
|
provider,
|
|
[LLMMessage(role=Role.user, content="Hi")],
|
|
thinking=level,
|
|
)
|
|
assert payload["reasoning_effort"] == level
|
|
|
|
def test_omitted_when_off(self, adapter, provider):
|
|
payload = _prepare(
|
|
adapter,
|
|
provider,
|
|
[LLMMessage(role=Role.user, content="Hi")],
|
|
thinking="off",
|
|
)
|
|
assert "reasoning_effort" not in payload
|
|
|
|
|
|
class TestThinkingBlocksConversion:
|
|
def test_assistant_with_reasoning_to_content_blocks(self, adapter, provider):
|
|
messages = [
|
|
LLMMessage(role=Role.user, content="Hi"),
|
|
LLMMessage(
|
|
role=Role.assistant,
|
|
content="Answer",
|
|
reasoning_content="Let me think...",
|
|
),
|
|
]
|
|
payload = _prepare(adapter, provider, messages, thinking="medium")
|
|
msg = payload["messages"][1]
|
|
assert msg["content"] == [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [{"type": "text", "text": "Let me think..."}],
|
|
},
|
|
{"type": "text", "text": "Answer"},
|
|
]
|
|
|
|
def test_assistant_without_reasoning_is_plain_string(self, adapter, provider):
|
|
messages = [
|
|
LLMMessage(role=Role.user, content="Hi"),
|
|
LLMMessage(role=Role.assistant, content="Hello"),
|
|
]
|
|
payload = _prepare(adapter, provider, messages)
|
|
assert payload["messages"][1]["content"] == "Hello"
|
|
|
|
def test_assistant_with_reasoning_and_tool_calls(self, adapter, provider):
|
|
messages = [
|
|
LLMMessage(role=Role.user, content="Hi"),
|
|
LLMMessage(
|
|
role=Role.assistant,
|
|
content="Let me search.",
|
|
reasoning_content="I should look this up.",
|
|
tool_calls=[
|
|
ToolCall(
|
|
id="tc_1",
|
|
index=0,
|
|
function=FunctionCall(name="search", arguments='{"q": "test"}'),
|
|
)
|
|
],
|
|
),
|
|
]
|
|
payload = _prepare(adapter, provider, messages, thinking="medium")
|
|
msg = payload["messages"][1]
|
|
assert msg["content"][0]["type"] == "thinking"
|
|
assert msg["content"][1] == {"type": "text", "text": "Let me search."}
|
|
assert msg["tool_calls"][0]["id"] == "tc_1"
|
|
assert msg["tool_calls"][0]["function"]["name"] == "search"
|
|
|
|
def test_tools_in_payload(self, adapter, provider):
|
|
tools = [
|
|
AvailableTool(
|
|
function=AvailableFunction(
|
|
name="search",
|
|
description="Search things",
|
|
parameters={"type": "object", "properties": {}},
|
|
)
|
|
)
|
|
]
|
|
payload = _prepare(
|
|
adapter, provider, [LLMMessage(role=Role.user, content="Hi")], tools=tools
|
|
)
|
|
assert len(payload["tools"]) == 1
|
|
assert payload["tools"][0]["function"]["name"] == "search"
|
|
|
|
|
|
class TestParseThinkingBlocks:
|
|
def test_string_content(self, adapter, provider):
|
|
data = {
|
|
"choices": [{"message": {"role": "assistant", "content": "Hello!"}}],
|
|
"usage": {"prompt_tokens": 10, "completion_tokens": 5},
|
|
}
|
|
chunk = adapter.parse_response(data, provider)
|
|
assert chunk.message.content == "Hello!"
|
|
assert chunk.message.reasoning_content is None
|
|
|
|
def test_thinking_and_text_blocks(self, adapter, provider):
|
|
data = {
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [
|
|
{"type": "text", "text": "Let me reason..."}
|
|
],
|
|
},
|
|
{"type": "text", "text": "Final answer"},
|
|
],
|
|
}
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
|
}
|
|
chunk = adapter.parse_response(data, provider)
|
|
assert chunk.message.content == "Final answer"
|
|
assert chunk.message.reasoning_content == "Let me reason..."
|
|
|
|
def test_multiple_thinking_inner_blocks(self, adapter, provider):
|
|
data = {
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [
|
|
{"type": "text", "text": "Step 1. "},
|
|
{"type": "text", "text": "Step 2."},
|
|
],
|
|
},
|
|
{"type": "text", "text": "Done"},
|
|
],
|
|
}
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
|
}
|
|
chunk = adapter.parse_response(data, provider)
|
|
assert chunk.message.reasoning_content == "Step 1. Step 2."
|
|
|
|
def test_tool_calls_in_response(self, adapter, provider):
|
|
data = {
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [
|
|
{"type": "text", "text": "need to search"}
|
|
],
|
|
},
|
|
{"type": "text", "text": "Searching..."},
|
|
],
|
|
"tool_calls": [
|
|
{
|
|
"id": "tc_1",
|
|
"index": 0,
|
|
"function": {
|
|
"name": "search",
|
|
"arguments": '{"q": "test"}',
|
|
},
|
|
}
|
|
],
|
|
}
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
|
}
|
|
chunk = adapter.parse_response(data, provider)
|
|
assert chunk.message.reasoning_content == "need to search"
|
|
assert chunk.message.content == "Searching..."
|
|
assert chunk.message.tool_calls[0].function.name == "search"
|
|
|
|
def test_streaming_text_delta_is_plain_string(self, adapter, provider):
|
|
data = {"choices": [{"delta": {"role": "assistant", "content": "Hi"}}]}
|
|
chunk = adapter.parse_response(data, provider)
|
|
assert chunk.message.content == "Hi"
|
|
assert chunk.message.reasoning_content is None
|
|
|
|
def test_thinking_delta_streaming(self, adapter, provider):
|
|
data = {
|
|
"choices": [
|
|
{
|
|
"delta": {
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [{"type": "text", "text": "hmm"}],
|
|
}
|
|
],
|
|
}
|
|
}
|
|
]
|
|
}
|
|
chunk = adapter.parse_response(data, provider)
|
|
assert chunk.message.reasoning_content == "hmm"
|