Remove trailing system→user conversion for cross-call stability
Perplexity appears to have statefulness within a conversation, so converting a system message to "user" in one call and then back to "system" in the next (after more messages are appended) causes API errors. Remove the trailing system→user conversion entirely — if the context only has system messages, the API call will fail but the mistake will be caught right away.
This commit is contained in:
@@ -80,15 +80,19 @@ class PerplexityLLMAdapter(OpenAILLMAdapter):
|
||||
(e.g. a converted system→user message adjacent to an existing user
|
||||
message gets merged).
|
||||
|
||||
3. **Ensure last message is user/tool** — If the last message is
|
||||
3. **Remove trailing assistant messages** — If the last message is
|
||||
"assistant", remove it. OpenAI appears to silently ignore trailing
|
||||
assistant messages server-side, so removing them preserves equivalent
|
||||
behavior while satisfying Perplexity's "last message must be
|
||||
user/tool" constraint. If the last message is "system", convert it
|
||||
to "user". A trailing system message can only occur when the context
|
||||
consists entirely of system messages (possibly followed by assistant
|
||||
messages that were just removed), because step 1 converts any system
|
||||
message that appears after a non-system message to "user".
|
||||
user/tool" constraint.
|
||||
|
||||
Note: we intentionally do *not* convert a trailing system message to
|
||||
"user". That would make the transformation unstable across calls —
|
||||
Perplexity appears to have statefulness/caching within a conversation,
|
||||
so a message that was sent as "user" in one call but becomes "system"
|
||||
in the next (once more messages are appended) causes errors. If the
|
||||
context consists entirely of system messages, the Perplexity API call
|
||||
will fail, but that mistake will be caught right away.
|
||||
|
||||
Args:
|
||||
messages: List of message dicts with "role" and "content" keys.
|
||||
@@ -138,17 +142,11 @@ class PerplexityLLMAdapter(OpenAILLMAdapter):
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Step 3: Handle trailing messages.
|
||||
# Step 3: Remove trailing assistant messages.
|
||||
# Perplexity requires the last message to be "user" or "tool".
|
||||
if messages:
|
||||
# Remove trailing assistant messages. OpenAI appears to silently
|
||||
# ignore trailing assistant messages server-side, so removing them
|
||||
# preserves equivalent behavior.
|
||||
while messages and messages[-1].get("role") == "assistant":
|
||||
messages.pop()
|
||||
|
||||
# If the last message is "system", convert it to "user".
|
||||
if messages and messages[-1].get("role") == "system":
|
||||
messages[-1]["role"] = "user"
|
||||
# OpenAI appears to silently ignore trailing assistant messages
|
||||
# server-side, so removing them preserves equivalent behavior.
|
||||
while messages and messages[-1].get("role") == "assistant":
|
||||
messages.pop()
|
||||
|
||||
return messages
|
||||
|
||||
@@ -1123,8 +1123,14 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase):
|
||||
self.assertEqual(params["messages"][0]["role"], "user")
|
||||
self.assertEqual(params["messages"][0]["content"], "Hello")
|
||||
|
||||
def test_only_system_message_converted_to_user(self):
|
||||
"""Test that a single system message is converted to user role."""
|
||||
def test_only_system_messages_preserved(self):
|
||||
"""Test that system-only contexts are left unchanged (no system→user conversion).
|
||||
|
||||
We intentionally do not convert trailing system messages to "user"
|
||||
because that would make the transformation unstable across calls —
|
||||
Perplexity has statefulness within a conversation, so a message that
|
||||
was "user" in one call but becomes "system" in the next causes errors.
|
||||
"""
|
||||
messages: list[LLMStandardMessage] = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
]
|
||||
@@ -1133,30 +1139,15 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase):
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
self.assertEqual(len(params["messages"]), 1)
|
||||
self.assertEqual(params["messages"][0]["role"], "user")
|
||||
self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.")
|
||||
|
||||
def test_only_system_messages_last_converted_to_user(self):
|
||||
"""Test that when only system messages exist, the last one is converted to user."""
|
||||
messages: list[LLMStandardMessage] = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "system", "content": "Always be polite."},
|
||||
]
|
||||
|
||||
context = LLMContext(messages=messages)
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
self.assertEqual(len(params["messages"]), 2)
|
||||
self.assertEqual(params["messages"][0]["role"], "system")
|
||||
self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.")
|
||||
self.assertEqual(params["messages"][1]["role"], "user")
|
||||
self.assertEqual(params["messages"][1]["content"], "Always be polite.")
|
||||
|
||||
def test_trailing_assistant_removed_then_system_converted(self):
|
||||
"""Test that trailing assistant is removed, exposing a system message that becomes user.
|
||||
def test_system_exposed_after_trailing_assistant_removed(self):
|
||||
"""Test that a system message exposed by trailing assistant removal stays system.
|
||||
|
||||
This exercises the ordering of step 3: strip trailing assistants first,
|
||||
then convert a trailing system to user.
|
||||
It's important that initial system messages are never converted to
|
||||
"user", because Perplexity has statefulness within a conversation — if
|
||||
a message was sent as "system" in one call and then becomes "user" in a
|
||||
later call (after more messages are appended), the API rejects it.
|
||||
"""
|
||||
messages: list[LLMStandardMessage] = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
@@ -1166,9 +1157,9 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase):
|
||||
context = LLMContext(messages=messages)
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
# Trailing assistant removed → [system] → system converted to user → [user]
|
||||
# Trailing assistant removed → [system], system stays as-is
|
||||
self.assertEqual(len(params["messages"]), 1)
|
||||
self.assertEqual(params["messages"][0]["role"], "user")
|
||||
self.assertEqual(params["messages"][0]["role"], "system")
|
||||
self.assertEqual(params["messages"][0]["content"], "You are helpful.")
|
||||
|
||||
def test_consecutive_assistants_merged_then_trailing_removed(self):
|
||||
|
||||
Reference in New Issue
Block a user