From 9a07bfa679efce23f870215913ef010d9ce43731 Mon Sep 17 00:00:00 2001 From: Raghav Sharma Date: Tue, 25 Nov 2025 23:45:38 -0800 Subject: [PATCH] fix(github_copilot): preserve encrypted_content in reasoning items for multi-turn conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Copilot uses encrypted_content in reasoning items to maintain conversation state across turns. The parent class (OpenAIResponsesAPIConfig._handle_reasoning_item) strips this field when converting to OpenAI's ResponseReasoningItem model, causing "encrypted content could not be verified" errors on multi-turn requests. This override preserves encrypted_content while still filtering out status=None which OpenAI's API rejects. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../responses/transformation.py | 38 ++++++++++ ...github_copilot_responses_transformation.py | 69 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/litellm/llms/github_copilot/responses/transformation.py b/litellm/llms/github_copilot/responses/transformation.py index cc96e3415f3..5ada02b3d99 100644 --- a/litellm/llms/github_copilot/responses/transformation.py +++ b/litellm/llms/github_copilot/responses/transformation.py @@ -182,6 +182,44 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig): # Return the responses endpoint return f"{api_base}/responses" + def _handle_reasoning_item(self, item: Dict[str, Any]) -> Dict[str, Any]: + """ + Handle reasoning items for GitHub Copilot, preserving encrypted_content. + + GitHub Copilot uses encrypted_content in reasoning items to maintain + conversation state across turns. The parent class strips this field + when converting to OpenAI's ResponseReasoningItem model, which causes + "encrypted content could not be verified" errors on multi-turn requests. + + This override preserves encrypted_content while still filtering out + status=None which OpenAI's API rejects. + """ + if item.get("type") == "reasoning": + # Preserve encrypted_content before parent processing + encrypted_content = item.get("encrypted_content") + + # Filter out None values for known problematic fields, + # but preserve encrypted_content even if it exists + filtered_item: Dict[str, Any] = {} + for k, v in item.items(): + # Always include encrypted_content if present (even if None) + if k == "encrypted_content": + if encrypted_content is not None: + filtered_item[k] = v + continue + # Filter out status=None which OpenAI API rejects + if k == "status" and v is None: + continue + # Include all other non-None values + if v is not None: + filtered_item[k] = v + + verbose_logger.debug( + f"GitHub Copilot reasoning item processed, encrypted_content preserved: {encrypted_content is not None}" + ) + return filtered_item + return item + # ==================== Helper Methods ==================== def _get_default_headers(self, api_key: str) -> Dict[str, str]: diff --git a/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py b/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py index d6032c61c60..1feb0244dbb 100644 --- a/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py +++ b/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py @@ -301,3 +301,72 @@ class TestGithubCopilotResponsesAPITransformation: for param in expected_params: assert param in supported, f"{param} should be in supported params" + + def test_handle_reasoning_item_preserves_encrypted_content(self): + """Test that _handle_reasoning_item preserves encrypted_content for GitHub Copilot. + + GitHub Copilot uses encrypted_content in reasoning items to maintain + conversation state across turns. This field must be preserved for + multi-turn conversations to work. + """ + config = GithubCopilotResponsesAPIConfig() + + reasoning_item = { + "type": "reasoning", + "id": "reasoning-123", + "summary": ["Step 1", "Step 2"], + "encrypted_content": "encrypted-blob-abc123", + "status": None, # Should be filtered out + "content": None, # Should be filtered out + } + + result = config._handle_reasoning_item(reasoning_item) + + # encrypted_content should be preserved + assert result.get("encrypted_content") == "encrypted-blob-abc123", ( + "encrypted_content must be preserved for GitHub Copilot multi-turn conversations" + ) + # status=None should be filtered out + assert "status" not in result, "status=None should be filtered out" + # content=None should be filtered out + assert "content" not in result, "content=None should be filtered out" + # Other fields should be preserved + assert result.get("type") == "reasoning" + assert result.get("id") == "reasoning-123" + assert result.get("summary") == ["Step 1", "Step 2"] + + def test_handle_reasoning_item_without_encrypted_content(self): + """Test _handle_reasoning_item when encrypted_content is not present""" + config = GithubCopilotResponsesAPIConfig() + + reasoning_item = { + "type": "reasoning", + "id": "reasoning-456", + "summary": ["Thinking..."], + "status": None, + } + + result = config._handle_reasoning_item(reasoning_item) + + # Should not have encrypted_content key at all + assert "encrypted_content" not in result + # status=None should be filtered out + assert "status" not in result + # Other fields preserved + assert result.get("type") == "reasoning" + assert result.get("id") == "reasoning-456" + + def test_handle_reasoning_item_non_reasoning_passthrough(self): + """Test _handle_reasoning_item passes through non-reasoning items unchanged""" + config = GithubCopilotResponsesAPIConfig() + + message_item = { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "Hello"}], + } + + result = config._handle_reasoning_item(message_item) + + # Non-reasoning items should pass through unchanged + assert result == message_item