diff --git a/src/pipecat/adapters/services/perplexity_adapter.py b/src/pipecat/adapters/services/perplexity_adapter.py index b55696893..a8fbe3c18 100644 --- a/src/pipecat/adapters/services/perplexity_adapter.py +++ b/src/pipecat/adapters/services/perplexity_adapter.py @@ -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 diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index 08710d77d..9cfeb8933 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -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):