项目文件夹

文件
wehub-resource-sync fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

1115 行
40 KiB
Python

"""Tests for the v1 API translator.
Covers request translation, response translation, streaming event
translation, continuation detection, and history conversion.
"""
import json
import pytest
from application.api.v1.translator import (
_get_client_tool_name,
_split_leaked_reasoning,
_strip_repr_quotes,
content_to_text,
convert_history,
extract_response_schema,
extract_system_prompt,
extract_tool_results,
is_continuation,
translate_request,
translate_response,
translate_stream_event,
)
# ---------------------------------------------------------------------------
# is_continuation
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestIsContinuation:
def test_normal_messages_not_continuation(self):
messages = [
{"role": "user", "content": "Hello"},
]
assert is_continuation(messages) is False
def test_tool_after_assistant_tool_calls_is_continuation(self):
messages = [
{"role": "user", "content": "What's the weather?"},
{
"role": "assistant",
"content": None,
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "get_weather", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": '{"temp": "72F"}'},
]
assert is_continuation(messages) is True
def test_assistant_without_tool_calls_not_continuation(self):
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi"},
{"role": "tool", "tool_call_id": "c1", "content": "result"},
]
# assistant has no tool_calls — not a valid continuation
assert is_continuation(messages) is False
def test_empty_messages(self):
assert is_continuation([]) is False
def test_multiple_tool_results(self):
messages = [
{"role": "user", "content": "Do stuff"},
{
"role": "assistant",
"tool_calls": [
{"id": "c1", "type": "function", "function": {"name": "a", "arguments": "{}"}},
{"id": "c2", "type": "function", "function": {"name": "b", "arguments": "{}"}},
],
},
{"role": "tool", "tool_call_id": "c1", "content": "r1"},
{"role": "tool", "tool_call_id": "c2", "content": "r2"},
]
assert is_continuation(messages) is True
# ---------------------------------------------------------------------------
# extract_tool_results
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestExtractToolResults:
def test_extracts_results(self):
messages = [
{"role": "assistant", "tool_calls": [{"id": "c1"}]},
{"role": "tool", "tool_call_id": "c1", "content": '{"temp": "72F"}'},
]
results = extract_tool_results(messages)
assert len(results) == 1
assert results[0]["call_id"] == "c1"
assert results[0]["result"] == {"temp": "72F"}
def test_string_content(self):
messages = [
{"role": "tool", "tool_call_id": "c1", "content": "plain text"},
]
results = extract_tool_results(messages)
assert results[0]["result"] == "plain text"
def test_multiple_results(self):
messages = [
{"role": "assistant", "tool_calls": []},
{"role": "tool", "tool_call_id": "c1", "content": "r1"},
{"role": "tool", "tool_call_id": "c2", "content": "r2"},
]
results = extract_tool_results(messages)
assert len(results) == 2
assert results[0]["call_id"] == "c1"
assert results[1]["call_id"] == "c2"
# ---------------------------------------------------------------------------
# convert_history
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestConvertHistory:
def test_user_assistant_pairs(self):
messages = [
{"role": "system", "content": "You are helpful"},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there"},
{"role": "user", "content": "How are you?"},
{"role": "assistant", "content": "I'm good"},
{"role": "user", "content": "What's 2+2?"}, # Last user = question
]
history = convert_history(messages)
assert len(history) == 2
assert history[0]["prompt"] == "Hello"
assert history[0]["response"] == "Hi there"
assert history[1]["prompt"] == "How are you?"
assert history[1]["response"] == "I'm good"
def test_single_user_message(self):
messages = [{"role": "user", "content": "Hi"}]
history = convert_history(messages)
assert history == []
def test_system_messages_skipped(self):
messages = [
{"role": "system", "content": "System prompt"},
{"role": "user", "content": "Question"},
]
history = convert_history(messages)
assert history == []
# ---------------------------------------------------------------------------
# extract_system_prompt
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestExtractSystemPrompt:
def test_extracts_first_system_message(self):
messages = [
{"role": "system", "content": "You are a pirate"},
{"role": "user", "content": "Hello"},
]
assert extract_system_prompt(messages) == "You are a pirate"
def test_returns_none_when_no_system_message(self):
messages = [{"role": "user", "content": "Hello"}]
assert extract_system_prompt(messages) is None
def test_returns_first_of_multiple_system_messages(self):
messages = [
{"role": "system", "content": "First"},
{"role": "system", "content": "Second"},
{"role": "user", "content": "Hello"},
]
assert extract_system_prompt(messages) == "First"
def test_empty_content_returns_empty_string(self):
messages = [
{"role": "system", "content": ""},
{"role": "user", "content": "Hello"},
]
assert extract_system_prompt(messages) == ""
def test_missing_content_returns_empty_string(self):
messages = [
{"role": "system"},
{"role": "user", "content": "Hello"},
]
assert extract_system_prompt(messages) == ""
# ---------------------------------------------------------------------------
# translate_request
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestTranslateRequest:
def test_normal_request(self):
data = {
"messages": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi"},
{"role": "user", "content": "What's 2+2?"},
],
}
result = translate_request(data, "test-key")
assert result["question"] == "What's 2+2?"
assert result["api_key"] == "test-key"
# v1 conversations persist but never list in the agent owner's
# sidebar; the translator no longer emits a display flag at all.
assert "save_conversation" not in result
history = json.loads(result["history"])
assert len(history) == 1
assert history[0]["prompt"] == "Hello"
def test_legacy_save_conversation_flag_is_ignored(self):
# Pre-visibility clients send ``docsgpt.save_conversation: true``
# meaning "persist" (the old contract). It must not leak into the
# internal request — visibility is not request-controllable on v1.
data = {
"messages": [{"role": "user", "content": "Hi"}],
"docsgpt": {"save_conversation": True},
}
result = translate_request(data, "key")
assert "save_conversation" not in result
def test_continuation_request(self):
data = {
"messages": [
{"role": "user", "content": "Search for X"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "search", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": '{"found": true}'},
],
}
result = translate_request(data, "key")
assert "tool_actions" in result
assert len(result["tool_actions"]) == 1
assert result["tool_actions"][0]["call_id"] == "c1"
def test_stateful_continuation_persists_by_default(self):
"""A stateful continuation (conversation_id present) implies the first
turn was saved, so the resumed turn must persist too (otherwise the
final answer + WAL row are lost)."""
data = {
"conversation_id": "conv-1",
"messages": [
{"role": "user", "content": "Search for X"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "search", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": "done"},
],
}
result = translate_request(data, "key")
assert result["persist"] is True
def test_stateless_continuation_does_not_persist_by_default(self):
"""A stateless continuation (no conversation_id, e.g. an OpenAI client
such as opencode) never persisted turn 1, so it must NOT default to
saving -- otherwise every tool round leaks an orphan conversation."""
data = {
"messages": [
{"role": "user", "content": "Search for X"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "search", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": "done"},
],
}
result = translate_request(data, "key")
assert result["persist"] is False
def test_stateful_continuation_persist_override_via_docsgpt(self):
"""``docsgpt.persist=false`` forces no persistence even with a
conversation_id."""
data = {
"conversation_id": "conv-1",
"docsgpt": {"persist": False},
"messages": [
{"role": "user", "content": "Search for X"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "search", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": "done"},
],
}
result = translate_request(data, "key")
assert result["persist"] is False
def test_continuation_ignores_legacy_save_conversation_flag(self):
"""``docsgpt.save_conversation`` is a dead flag — persist is separate."""
data = {
"docsgpt": {"save_conversation": True},
"messages": [
{"role": "user", "content": "Search for X"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "search", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": "done"},
],
}
result = translate_request(data, "key")
assert "save_conversation" not in result
# Stateless continuation (no conversation_id) still skips persistence.
assert result["persist"] is False
def test_continuation_with_top_level_conversation_id(self):
"""Standard clients send conversation_id at request level, not in messages."""
data = {
"conversation_id": "conv-top-level",
"messages": [
{"role": "user", "content": "Do stuff"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "act", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": "done"},
],
}
result = translate_request(data, "key")
assert result["conversation_id"] == "conv-top-level"
def test_continuation_in_message_conversation_id_takes_precedence(self):
"""When both in-message and top-level conversation_id exist, in-message wins."""
data = {
"conversation_id": "conv-top-level",
"messages": [
{"role": "user", "content": "Do stuff"},
{
"role": "assistant",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "act", "arguments": "{}"}}],
"docsgpt": {"conversation_id": "conv-in-message"},
},
{"role": "tool", "tool_call_id": "c1", "content": "done"},
],
}
result = translate_request(data, "key")
assert result["conversation_id"] == "conv-in-message"
def test_client_tools_passed_through(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"tools": [{"type": "function", "function": {"name": "my_tool"}}],
}
result = translate_request(data, "key")
assert result["client_tools"] == data["tools"]
def test_docsgpt_attachments(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"docsgpt": {"attachments": ["att1", "att2"]},
}
result = translate_request(data, "key")
assert result["attachments"] == ["att1", "att2"]
def test_system_prompt_override_included_when_present(self):
data = {
"messages": [
{"role": "system", "content": "Custom prompt"},
{"role": "user", "content": "Hello"},
],
}
result = translate_request(data, "key")
assert result["system_prompt_override"] == "Custom prompt"
def test_system_prompt_override_absent_when_no_system_message(self):
data = {
"messages": [{"role": "user", "content": "Hello"}],
}
result = translate_request(data, "key")
assert "system_prompt_override" not in result
# ---------------------------------------------------------------------------
# translate_response
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestTranslateResponse:
def test_basic_response(self):
resp = translate_response(
conversation_id="conv-1",
answer="Hello!",
sources=[],
tool_calls=[],
thought="",
model_name="my-agent",
)
assert resp["id"] == "chatcmpl-conv-1"
assert resp["object"] == "chat.completion"
assert resp["model"] == "my-agent"
assert resp["choices"][0]["message"]["content"] == "Hello!"
assert resp["choices"][0]["finish_reason"] == "stop"
assert "reasoning_content" not in resp["choices"][0]["message"]
def test_response_with_thought(self):
resp = translate_response(
conversation_id="c1",
answer="Result",
sources=[],
tool_calls=[],
thought="Thinking about it...",
model_name="agent",
)
assert resp["choices"][0]["message"]["reasoning_content"] == "Thinking about it..."
def test_response_with_sources(self):
sources = [{"title": "doc.txt", "text": "content", "source": "/doc.txt"}]
resp = translate_response(
conversation_id="c1",
answer="Found it",
sources=sources,
tool_calls=[],
thought="",
model_name="agent",
)
assert resp["docsgpt"]["sources"] == sources
def test_response_with_tool_calls(self):
tool_calls = [{"tool_name": "notes", "call_id": "c1", "artifact_id": "a1"}]
resp = translate_response(
conversation_id="c1",
answer="Done",
sources=[],
tool_calls=tool_calls,
thought="",
model_name="agent",
)
assert resp["docsgpt"]["tool_calls"] == tool_calls
def test_pending_tool_calls_uses_tool_name(self):
"""Client tool responses use the original tool_name, not the LLM-visible action_name."""
pending = [
{
"call_id": "c1",
"tool_name": "get_weather",
"action_name": "get_weather",
"arguments": {"city": "SF"},
}
]
resp = translate_response(
conversation_id="c1",
answer="",
sources=[],
tool_calls=[],
thought="",
model_name="agent",
pending_tool_calls=pending,
)
tc = resp["choices"][0]["message"]["tool_calls"][0]
assert tc["function"]["name"] == "get_weather"
def test_pending_tool_calls_tool_name_takes_precedence(self):
"""When tool_name differs from action_name, tool_name is used."""
pending = [
{
"call_id": "c1",
"tool_name": "search",
"action_name": "search_1",
"arguments": {"q": "test"},
}
]
resp = translate_response(
conversation_id="c1",
answer="",
sources=[],
tool_calls=[],
thought="",
model_name="agent",
pending_tool_calls=pending,
)
tc = resp["choices"][0]["message"]["tool_calls"][0]
assert tc["function"]["name"] == "search"
def test_pending_tool_calls(self):
pending = [
{
"call_id": "c1",
"name": "get_weather",
"arguments": {"city": "SF"},
}
]
resp = translate_response(
conversation_id="c1",
answer="",
sources=[],
tool_calls=[],
thought="",
model_name="agent",
pending_tool_calls=pending,
)
assert resp["choices"][0]["finish_reason"] == "tool_calls"
assert resp["choices"][0]["message"]["content"] is None
assert len(resp["choices"][0]["message"]["tool_calls"]) == 1
tc = resp["choices"][0]["message"]["tool_calls"][0]
assert tc["id"] == "c1"
assert tc["function"]["name"] == "get_weather"
def test_no_docsgpt_when_empty(self):
resp = translate_response(
conversation_id="",
answer="Hi",
sources=None,
tool_calls=None,
thought="",
model_name="agent",
)
assert "docsgpt" not in resp
# ---------------------------------------------------------------------------
# translate_stream_event
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestTranslateStreamEvent:
def test_answer_event(self):
chunks = translate_stream_event(
{"type": "answer", "answer": "Hello"},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["choices"][0]["delta"]["content"] == "Hello"
def test_thought_event(self):
chunks = translate_stream_event(
{"type": "thought", "thought": "reasoning"},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["choices"][0]["delta"]["reasoning_content"] == "reasoning"
def test_source_event(self):
chunks = translate_stream_event(
{"type": "source", "source": [{"title": "t", "text": "x"}]},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["docsgpt"]["type"] == "source"
assert len(parsed["docsgpt"]["sources"]) == 1
def test_end_event(self):
chunks = translate_stream_event(
{"type": "end"},
"chatcmpl-1", "agent",
)
assert len(chunks) == 2
# First chunk: finish_reason stop
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["choices"][0]["finish_reason"] == "stop"
# Second chunk: [DONE]
assert chunks[1].strip() == "data: [DONE]"
def test_tool_call_client_execution(self):
chunks = translate_stream_event(
{
"type": "tool_call",
"data": {
"call_id": "c1",
"action_name": "get_weather",
"arguments": {"city": "SF"},
"status": "requires_client_execution",
},
},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
tc = parsed["choices"][0]["delta"]["tool_calls"][0]
assert tc["id"] == "c1"
assert tc["function"]["name"] == "get_weather"
def test_tool_call_client_execution_uses_tool_name(self):
"""Streaming tool calls use tool_name (original name) for client responses."""
chunks = translate_stream_event(
{
"type": "tool_call",
"data": {
"call_id": "c1",
"tool_name": "create",
"action_name": "create",
"arguments": {"title": "test"},
"status": "requires_client_execution",
},
},
"chatcmpl-1", "agent",
)
parsed = json.loads(chunks[0].replace("data: ", "").strip())
tc = parsed["choices"][0]["delta"]["tool_calls"][0]
assert tc["function"]["name"] == "create"
def test_tool_call_completed(self):
chunks = translate_stream_event(
{
"type": "tool_call",
"data": {
"call_id": "c1",
"status": "completed",
"result": "done",
"artifact_id": "a1",
},
},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["docsgpt"]["type"] == "tool_call"
assert parsed["docsgpt"]["data"]["artifact_id"] == "a1"
def test_tool_calls_pending(self):
chunks = translate_stream_event(
{
"type": "tool_calls_pending",
"data": {"pending_tool_calls": [{"call_id": "c1"}]},
},
"chatcmpl-1", "agent",
)
assert len(chunks) == 2
# Standard chunk with finish_reason tool_calls
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["choices"][0]["finish_reason"] == "tool_calls"
# Extension chunk
ext = json.loads(chunks[1].replace("data: ", "").strip())
assert ext["docsgpt"]["type"] == "tool_calls_pending"
def test_id_event(self):
chunks = translate_stream_event(
{"type": "id", "id": "conv-123"},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["docsgpt"]["conversation_id"] == "conv-123"
def test_error_event(self):
chunks = translate_stream_event(
{"type": "error", "error": "Something went wrong"},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["error"]["message"] == "Something went wrong"
def test_tool_calls_event_skipped(self):
"""The aggregate tool_calls event is redundant and should be skipped."""
chunks = translate_stream_event(
{"type": "tool_calls", "tool_calls": [{"call_id": "c1"}]},
"chatcmpl-1", "agent",
)
assert len(chunks) == 0
def test_research_events_skipped(self):
assert translate_stream_event(
{"type": "research_plan", "data": {}}, "id", "m"
) == []
assert translate_stream_event(
{"type": "research_progress", "data": {}}, "id", "m"
) == []
def test_awaiting_approval_as_extension(self):
chunks = translate_stream_event(
{
"type": "tool_call",
"data": {"call_id": "c1", "status": "awaiting_approval"},
},
"chatcmpl-1", "agent",
)
assert len(chunks) == 1
parsed = json.loads(chunks[0].replace("data: ", "").strip())
assert parsed["docsgpt"]["type"] == "tool_call"
def test_standard_clients_can_ignore_docsgpt(self):
"""The docsgpt namespace rides on a valid empty chat.completion.chunk.
Standard OpenAI clients validate every streamed frame as a
``chat.completion.chunk``; a frame without ``choices`` is rejected.
So docsgpt-only events (sources, ids, tool_calls) are emitted as a
valid chunk with an empty delta plus a ``docsgpt`` extension that
standard parsers simply ignore.
"""
chunks = translate_stream_event(
{"type": "source", "source": [{"title": "t"}]},
"chatcmpl-1", "agent",
)
parsed = json.loads(chunks[0].replace("data: ", "").strip())
# Valid chunk envelope: standard parsers read choices[0].delta (empty)
assert isinstance(parsed.get("choices"), list)
assert parsed["choices"][0]["delta"] == {}
# docsgpt extension is present for clients that want it
assert "docsgpt" in parsed
# ---------------------------------------------------------------------------
# _get_client_tool_name
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGetClientToolName:
def test_uses_tool_name_when_present(self):
assert _get_client_tool_name({"tool_name": "create", "action_name": "create_1"}) == "create"
def test_falls_back_to_action_name(self):
assert _get_client_tool_name({"action_name": "get_weather"}) == "get_weather"
def test_falls_back_to_name(self):
assert _get_client_tool_name({"name": "search"}) == "search"
def test_returns_empty_when_no_fields(self):
assert _get_client_tool_name({}) == ""
# ---------------------------------------------------------------------------
# content_to_text
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestContentToText:
def test_plain_string_passthrough(self):
assert content_to_text("hello") == "hello"
def test_none_returns_empty(self):
assert content_to_text(None) == ""
def test_text_parts_joined(self):
content = [
{"type": "text", "text": "line one"},
{"type": "text", "text": "line two"},
]
assert content_to_text(content) == "line one\nline two"
def test_image_parts_dropped(self):
content = [
{"type": "text", "text": "describe this"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,xxx"}},
]
# Only the text part contributes; the image is dropped from the flattened text.
assert content_to_text(content) == "describe this"
def test_bare_string_parts_included(self):
assert content_to_text(["a", "b"]) == "a\nb"
def test_missing_text_field_becomes_empty(self):
assert content_to_text([{"type": "text"}]) == ""
def test_non_string_non_list_coerced(self):
assert content_to_text(123) == "123"
# ---------------------------------------------------------------------------
# extract_response_schema
# ---------------------------------------------------------------------------
_SCHEMA = {
"type": "object",
"properties": {"answer": {"type": "string"}},
"required": ["answer"],
}
@pytest.mark.unit
class TestExtractResponseSchema:
def test_none_when_absent(self):
assert extract_response_schema({}) is None
def test_response_format_json_schema_wrapper(self):
data = {
"response_format": {
"type": "json_schema",
"json_schema": {"name": "ans", "schema": _SCHEMA},
}
}
assert extract_response_schema(data) == _SCHEMA
def test_response_format_bare_schema_under_json_schema(self):
# A bare schema (no "schema" wrapper) but with a top-level "type" is tolerated.
data = {
"response_format": {
"type": "json_schema",
"json_schema": _SCHEMA,
}
}
assert extract_response_schema(data) == _SCHEMA
def test_response_format_json_object_carries_no_schema(self):
data = {"response_format": {"type": "json_object"}}
assert extract_response_schema(data) is None
def test_response_schema_raw_object(self):
assert extract_response_schema({"response_schema": _SCHEMA}) == _SCHEMA
def test_response_schema_wrapper(self):
data = {"response_schema": {"schema": _SCHEMA}}
assert extract_response_schema(data) == _SCHEMA
def test_response_schema_takes_precedence_over_response_format(self):
other = {"type": "object", "properties": {}}
data = {
"response_schema": _SCHEMA,
"response_format": {
"type": "json_schema",
"json_schema": {"schema": other},
},
}
assert extract_response_schema(data) == _SCHEMA
# ---------------------------------------------------------------------------
# _split_leaked_reasoning
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSplitLeakedReasoning:
def test_no_marker_is_noop(self):
assert _split_leaked_reasoning("just an answer") == ("just an answer", "")
def test_none_is_noop(self):
assert _split_leaked_reasoning(None) == (None, "")
def test_extracts_single_thought(self):
content = "{'type': 'thought', 'thought': 'let me think'}The answer is 42."
clean, leaked = _split_leaked_reasoning(content)
assert clean == "The answer is 42."
assert leaked == "let me think"
def test_extracts_multiple_thoughts(self):
content = (
"{'type': 'thought', 'thought': 'first'}"
"partial"
"{'type': 'thought', 'thought': 'second'}done"
)
clean, leaked = _split_leaked_reasoning(content)
assert clean == "partialdone"
assert leaked == "firstsecond"
def test_double_quoted_thought_value(self):
# When the token contains an apostrophe the repr uses double quotes.
content = "{'type': 'thought', 'thought': \"I'll check\"}answer"
clean, leaked = _split_leaked_reasoning(content)
assert clean == "answer"
assert leaked == "I'll check"
def test_thought_value_with_brace_not_truncated(self):
content = "{'type': 'thought', 'thought': 'use {json} here'}final"
clean, leaked = _split_leaked_reasoning(content)
assert clean == "final"
assert leaked == "use {json} here"
def test_strip_repr_quotes_unquoted_passthrough(self):
# Defensive branch: an unquoted value is returned unchanged.
assert _strip_repr_quotes("plain") == "plain"
assert _strip_repr_quotes("'quoted'") == "quoted"
# ---------------------------------------------------------------------------
# translate_request — structured outputs / sampling / multimodal
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestTranslateRequestStructuredOutputs:
def test_response_format_surfaces_json_schema_strict_default(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "ans", "schema": _SCHEMA},
},
}
result = translate_request(data, "key")
assert result["json_schema"] == _SCHEMA
assert result["json_schema_strict"] is True
def test_response_format_honours_explicit_strict_false(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "ans", "schema": _SCHEMA, "strict": False},
},
}
result = translate_request(data, "key")
assert result["json_schema_strict"] is False
def test_json_object_mode_flag(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"response_format": {"type": "json_object"},
}
result = translate_request(data, "key")
assert result["json_object"] is True
assert "json_schema" not in result
def test_sampling_params_forwarded(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"temperature": 0.2,
"top_p": 0.9,
"seed": 7,
}
result = translate_request(data, "key")
assert result["llm_params"] == {"temperature": 0.2, "top_p": 0.9, "seed": 7}
def test_max_tokens_alias_dropped_when_canonical_present(self):
data = {
"messages": [{"role": "user", "content": "Hi"}],
"max_tokens": 100,
"max_completion_tokens": 200,
}
result = translate_request(data, "key")
assert result["llm_params"]["max_completion_tokens"] == 200
assert "max_tokens" not in result["llm_params"]
def test_multimodal_content_preserved_and_question_flattened(self):
content = [
{"type": "text", "text": "what is this"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,xxx"}},
]
data = {"messages": [{"role": "user", "content": content}]}
result = translate_request(data, "key")
assert result["question"] == "what is this"
assert result["multimodal_content"] == content
def test_plain_text_request_has_no_multimodal_content(self):
data = {"messages": [{"role": "user", "content": "plain"}]}
result = translate_request(data, "key")
assert "multimodal_content" not in result
def test_continuation_forwards_schema_and_sampling(self):
data = {
"messages": [
{"role": "user", "content": "Hi"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "result"},
],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "ans", "schema": _SCHEMA},
},
"temperature": 0.5,
}
result = translate_request(data, "key")
assert result["tool_actions"]
assert result["json_schema"] == _SCHEMA
assert result["json_schema_strict"] is True
assert result["llm_params"] == {"temperature": 0.5}
def test_continuation_forwards_tools_and_json_object(self):
data = {
"messages": [
{"role": "user", "content": "Hi"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "result"},
],
"tools": [{"type": "function", "function": {"name": "search"}}],
"response_format": {"type": "json_object"},
}
result = translate_request(data, "key")
assert result["client_tools"] == data["tools"]
assert result["json_object"] is True
# ---------------------------------------------------------------------------
# translate_response / translate_stream_event — reasoning-leak handling
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestReasoningLeakHandling:
def test_response_strips_leak_only_when_enabled(self):
answer = "{'type': 'thought', 'thought': 'hmm'}Final answer."
result = translate_response(
conversation_id="",
answer=answer,
sources=None,
tool_calls=None,
thought="",
model_name="agent",
strip_reasoning_leak=True,
)
msg = result["choices"][0]["message"]
assert msg["content"] == "Final answer."
assert msg["reasoning_content"] == "hmm"
def test_response_preserves_content_when_strip_disabled(self):
answer = "{'type': 'thought', 'thought': 'hmm'}Final answer."
result = translate_response(
conversation_id="",
answer=answer,
sources=None,
tool_calls=None,
thought="",
model_name="agent",
strip_reasoning_leak=False,
)
msg = result["choices"][0]["message"]
# Untouched: the leak marker is left in content verbatim.
assert msg["content"] == answer
assert "reasoning_content" not in msg
def test_response_combines_thought_and_leaked_reasoning(self):
answer = "{'type': 'thought', 'thought': 'leaked'}Answer."
result = translate_response(
conversation_id="",
answer=answer,
sources=None,
tool_calls=None,
thought="explicit ",
model_name="agent",
strip_reasoning_leak=True,
)
msg = result["choices"][0]["message"]
assert msg["reasoning_content"] == "explicit leaked"
def test_stream_answer_splits_leak_when_enabled(self):
chunks = translate_stream_event(
{"type": "answer", "answer": "{'type': 'thought', 'thought': 'r'}hi"},
"chatcmpl-1", "agent", True,
)
deltas = [
json.loads(c.replace("data: ", "").strip())["choices"][0]["delta"]
for c in chunks
]
assert {"reasoning_content": "r"} in deltas
assert {"content": "hi"} in deltas
def test_stream_answer_preserves_content_when_disabled(self):
raw = "{'type': 'thought', 'thought': 'r'}hi"
chunks = translate_stream_event(
{"type": "answer", "answer": raw}, "chatcmpl-1", "agent", False,
)
deltas = [
json.loads(c.replace("data: ", "").strip())["choices"][0]["delta"]
for c in chunks
]
assert {"content": raw} in deltas
assert all("reasoning_content" not in d for d in deltas)
def test_stream_structured_answer_event(self):
chunks = translate_stream_event(
{"type": "structured_answer", "answer": '{"answer": "42"}'},
"chatcmpl-1", "agent", True,
)
deltas = [
json.loads(c.replace("data: ", "").strip())["choices"][0]["delta"]
for c in chunks
]
assert {"content": '{"answer": "42"}'} in deltas
def test_stream_structured_answer_splits_leak(self):
chunks = translate_stream_event(
{
"type": "structured_answer",
"answer": "{'type': 'thought', 'thought': 'why'}{\"answer\": \"42\"}",
},
"chatcmpl-1", "agent", True,
)
deltas = [
json.loads(c.replace("data: ", "").strip())["choices"][0]["delta"]
for c in chunks
]
assert {"reasoning_content": "why"} in deltas
assert {"content": '{"answer": "42"}'} in deltas