fix(github_copilot): preserve encrypted_content in reasoning items for multi-turn conversations

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 <noreply@anthropic.com>
This commit is contained in:
Raghav Sharma 2025-11-25 23:45:38 -08:00
parent 6e5c7c0008
commit 9a07bfa679
2 changed files with 107 additions and 0 deletions

View file

@ -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]:

View file

@ -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