Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ALL_RESPONSES_API_TOOL_PARAMS,
AllMessageValues,
ChatCompletionImageObject,
ChatCompletionRedactedThinkingBlock,
ChatCompletionThinkingBlock,
OpenAIMessageContentListBlock,
)
Expand Down Expand Up @@ -198,35 +199,120 @@ def convert_chat_completion_messages_to_responses_api(
"output": tool_output,
}
)
elif role == "assistant" and tool_calls and isinstance(tool_calls, list):
for tool_call in tool_calls:
function = tool_call.get("function")
if function:
input_tool_call = {
"type": "function_call",
"call_id": tool_call["id"],
else:
# Handle assistant and user messages
# For assistant: extract thinking_blocks FIRST (before content/tool_calls)
if role == "assistant" and "thinking_blocks" in msg:
thinking_blocks = msg.get("thinking_blocks")
if thinking_blocks and isinstance(thinking_blocks, list):
reasoning_items = self._convert_thinking_blocks_to_reasoning_items(
thinking_blocks
)
input_items.extend(reasoning_items)
verbose_logger.debug(
f"Responses API: Added {len(reasoning_items)} reasoning items"
)

# THEN handle message content: tool_calls OR regular content
if role == "assistant" and tool_calls and isinstance(tool_calls, list):
# Handle tool calls
for tool_call in tool_calls:
function = tool_call.get("function")
if function:
input_tool_call = {
"type": "function_call",
"call_id": tool_call["id"],
}
if "name" in function:
input_tool_call["name"] = function["name"]
if "arguments" in function:
input_tool_call["arguments"] = function["arguments"]
input_items.append(input_tool_call)
else:
raise ValueError(f"tool call not supported: {tool_call}")
elif content is not None:
# Handle regular message content (user or assistant)
input_items.append(
{
"type": "message",
"role": role,
"content": self._convert_content_to_responses_format(
content, cast(str, role)
),
}
if "name" in function:
input_tool_call["name"] = function["name"]
if "arguments" in function:
input_tool_call["arguments"] = function["arguments"]
input_items.append(input_tool_call)
else:
raise ValueError(f"tool call not supported: {tool_call}")
elif content is not None:
# Regular user/assistant message
input_items.append(
{
"type": "message",
"role": role,
"content": self._convert_content_to_responses_format(
content, cast(str, role)
),
}
)
)

return input_items, instructions

def _convert_thinking_blocks_to_reasoning_items(
self,
thinking_blocks: List[
Union["ChatCompletionThinkingBlock", "ChatCompletionRedactedThinkingBlock"]
],
) -> List[Dict[str, Any]]:
"""
Convert thinking_blocks to reasoning items for Responses API.

Args:
thinking_blocks: List of thinking blocks from Chat Completions format

Returns:
List of reasoning items in Responses API format

Handles two types of thinking blocks:
- type: "thinking" -> Creates reasoning item with full summary text preserved
- type: "redacted_thinking" -> Creates reasoning item with encrypted_content preserved

Per OpenAI docs, reasoning items should be passed back "untouched" between turns.
See: https://developers.openai.com/api/docs/guides/reasoning
"""
reasoning_items: List[Dict[str, Any]] = []

for block in thinking_blocks:
block_type = block.get("type")

if block_type == "thinking":
# Regular thinking -> reasoning with summary (full text, no truncation)
thinking_text = block.get("thinking", "")

reasoning_item: Dict[str, Any] = {
"type": "reasoning",
"summary": [{"type": "summary_text", "text": thinking_text}] if thinking_text else [],
}

reasoning_items.append(reasoning_item)
verbose_logger.debug(
"Responses API: Converted thinking block to reasoning item with summary"
)

elif block_type == "redacted_thinking":
# Redacted thinking -> reasoning with encrypted_content
encrypted_data = block.get("data")

reasoning_item = {
"type": "reasoning",
# OpenAI requires empty summary array with encrypted_content
# See: https://developers.openai.com/cookbook/examples/responses_api/reasoning_items
"summary": [],
}

# Preserve encrypted_content if present
if encrypted_data is not None:
reasoning_item["encrypted_content"] = encrypted_data

reasoning_items.append(reasoning_item)
verbose_logger.debug(
f"Responses API: Converted redacted_thinking block, encrypted_content preserved: {encrypted_data is not None}"
)

else:
# Unknown type - skip with warning
verbose_logger.warning(
f"Responses API: Unknown thinking block type '{block_type}', skipping"
)

return reasoning_items

def _map_optional_params_to_responses_api_request(
self,
optional_params: dict,
Expand Down Expand Up @@ -407,6 +493,7 @@ def _convert_response_output_to_choices(
choices: List[Choices] = []
index = 0
reasoning_content: Optional[str] = None
thinking_blocks_list: List[Dict[str, Any]] = []

# Collect all tool calls to put them in a single choice
# (Chat Completions API expects all tool calls in one message)
Expand All @@ -415,10 +502,39 @@ def _convert_response_output_to_choices(

for item in output_items:
if isinstance(item, ResponseReasoningItem):
# Reset reasoning_content for each reasoning item to avoid stale data
reasoning_content = None

# Extract reasoning_content from summary
for summary_item in item.summary:
response_text = getattr(summary_item, "text", "")
reasoning_content = response_text if response_text else ""

# Extract encrypted_content and convert to thinking_blocks
encrypted_content = getattr(item, "encrypted_content", None)
if encrypted_content is not None:
# Create redacted_thinking block with encrypted_content
thinking_block: Dict[str, Any] = {
"type": "redacted_thinking",
"data": encrypted_content,
}
thinking_blocks_list.append(thinking_block)
verbose_logger.debug(
f"Responses API: Extracted encrypted_content from ResponseReasoningItem, "
f"converted to thinking_blocks (type: redacted_thinking)"
)
elif reasoning_content:
# If no encrypted_content but have reasoning text, create regular thinking block
thinking_block = {
"type": "thinking",
"thinking": reasoning_content,
}
thinking_blocks_list.append(thinking_block)
verbose_logger.debug(
f"Responses API: Extracted reasoning text from ResponseReasoningItem, "
f"converted to thinking_blocks (type: thinking)"
)

elif isinstance(item, ResponseOutputMessage):
for content in item.content:
response_text = getattr(content, "text", "")
Expand All @@ -431,6 +547,7 @@ def _convert_response_output_to_choices(
role=item.role,
content=response_text if response_text else "",
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks_list if thinking_blocks_list else None,
annotations=annotations,
)

Expand All @@ -443,6 +560,7 @@ def _convert_response_output_to_choices(
)

reasoning_content = None # flush reasoning content
thinking_blocks_list = [] # flush thinking blocks
index += 1

elif isinstance(item, ResponseFunctionToolCall):
Expand Down Expand Up @@ -471,11 +589,13 @@ def _convert_response_output_to_choices(
content=None,
tool_calls=accumulated_tool_calls,
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks_list if thinking_blocks_list else None,
)
choices.append(
Choices(message=msg, finish_reason="tool_calls", index=index)
)
reasoning_content = None
thinking_blocks_list = []

return choices

Expand Down
Loading
Loading