mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
Preserve OpenAI encrypted reasoning blocks
This commit is contained in:
@@ -10,7 +10,7 @@ use crate::{
|
||||
protocol::canonical::{
|
||||
canonical_content_block_to_openai_responses_part, canonical_extension_object_mut,
|
||||
canonical_usage_to_openai_responses_usage, canonicalize_tool_arguments,
|
||||
flush_openai_responses_message_item, namespace_extension_object,
|
||||
flush_openai_responses_message_item, is_openai_thinking_block, namespace_extension_object,
|
||||
openai_responses_extensions, openai_responses_output_to_canonical_blocks,
|
||||
openai_usage_to_canonical, CanonicalContentBlock, CanonicalResponse,
|
||||
CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
|
||||
@@ -158,8 +158,16 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b
|
||||
CanonicalContentBlock::Thinking {
|
||||
text,
|
||||
encrypted_content,
|
||||
extensions,
|
||||
..
|
||||
} => {
|
||||
let encrypted_content = encrypted_content
|
||||
.as_ref()
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|_| is_openai_thinking_block(extensions));
|
||||
if text.trim().is_empty() && encrypted_content.is_none() {
|
||||
continue;
|
||||
}
|
||||
flush_openai_responses_message_item(
|
||||
&mut output,
|
||||
&mut message_content,
|
||||
@@ -173,9 +181,7 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b
|
||||
Value::String(format!("{}_rs_{}", response_id, output.len())),
|
||||
);
|
||||
item.insert("status".to_string(), Value::String("completed".to_string()));
|
||||
if let Some(encrypted_content) =
|
||||
encrypted_content.as_ref().filter(|value| !value.is_empty())
|
||||
{
|
||||
if let Some(encrypted_content) = encrypted_content {
|
||||
item.insert(
|
||||
"encrypted_content".to_string(),
|
||||
Value::String(encrypted_content.clone()),
|
||||
@@ -533,6 +539,65 @@ mod tests {
|
||||
assert_eq!(body["conversation"]["id"], "conv_123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_response_parser_preserves_encrypted_reasoning_without_summary() {
|
||||
let body = json!({
|
||||
"id": "resp_test",
|
||||
"model": "gpt-5",
|
||||
"status": "completed",
|
||||
"output": [{
|
||||
"type": "reasoning",
|
||||
"id": "rs_1",
|
||||
"status": "completed",
|
||||
"summary": [],
|
||||
"encrypted_content": "openai-opaque"
|
||||
}]
|
||||
});
|
||||
|
||||
let canonical = from_raw(&body).expect("response should parse");
|
||||
|
||||
assert!(matches!(
|
||||
canonical.content.first(),
|
||||
Some(CanonicalContentBlock::Thinking {
|
||||
text,
|
||||
encrypted_content,
|
||||
..
|
||||
}) if text.is_empty() && encrypted_content.as_deref() == Some("openai-opaque")
|
||||
));
|
||||
|
||||
let rebuilt = to_raw(&canonical, &json!({}), false);
|
||||
assert_eq!(rebuilt["output"][0]["type"], "reasoning");
|
||||
assert_eq!(
|
||||
rebuilt["output"][0]["encrypted_content"],
|
||||
json!("openai-opaque")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_response_builder_does_not_emit_claude_redacted_as_openai_encrypted_content() {
|
||||
let mut extensions = BTreeMap::new();
|
||||
extensions.insert("aether".to_string(), json!({"source": "claude_thinking"}));
|
||||
let response = CanonicalResponse {
|
||||
id: "msg_claude".to_string(),
|
||||
model: "claude-sonnet".to_string(),
|
||||
content: vec![CanonicalContentBlock::Thinking {
|
||||
text: String::new(),
|
||||
signature: None,
|
||||
encrypted_content: Some("{\"type\":\"redacted_thinking\",\"v\":5}".to_string()),
|
||||
extensions,
|
||||
}],
|
||||
outputs: Vec::new(),
|
||||
stop_reason: Some(CanonicalStopReason::EndTurn),
|
||||
usage: None,
|
||||
extensions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let body = to_raw(&response, &json!({}), false);
|
||||
|
||||
assert!(body["output"].as_array().expect("output").is_empty());
|
||||
assert!(!body.to_string().contains("encrypted_content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_response_parser_reads_web_search_call_as_tool_use() {
|
||||
let body = json!({
|
||||
|
||||
@@ -16,6 +16,7 @@ const CLAUDE_MESSAGES_REQUEST_SOURCE_MARKER: &str = "claude_messages_request";
|
||||
const CLAUDE_SYSTEM_SOURCE_MARKER: &str = "claude_system";
|
||||
const CLAUDE_THINKING_SOURCE_MARKER: &str = "claude_thinking";
|
||||
const CLAUDE_TOOL_RESULT_SOURCE_MARKER: &str = "claude_tool_result";
|
||||
const OPENAI_THINKING_SOURCE_MARKER: &str = "openai_thinking";
|
||||
const OPENAI_CHAT_TOOL_RESULT_SOURCE_MARKER: &str = "openai_chat_tool_result";
|
||||
const OPENAI_RESPONSES_TOOL_RESULT_SOURCE_MARKER: &str = "openai_responses_tool_result";
|
||||
const OPENAI_CHAT_TOOL_ERROR_PREFIX: &str = "[tool error]";
|
||||
@@ -1451,6 +1452,7 @@ pub(crate) fn openai_message_content_blocks(
|
||||
let mut extensions = BTreeMap::new();
|
||||
canonical_extension_object_mut(&mut extensions, "openai")
|
||||
.insert("omit_reasoning_parts".to_string(), Value::Bool(true));
|
||||
let extensions = openai_thinking_extensions(extensions);
|
||||
blocks.insert(
|
||||
0,
|
||||
CanonicalContentBlock::Thinking {
|
||||
@@ -1571,6 +1573,7 @@ pub(crate) fn openai_reasoning_blocks(message: &Map<String, Value>) -> Vec<Canon
|
||||
canonical_extension_object_mut(&mut extensions, "openai")
|
||||
.insert("omit_reasoning_content".to_string(), Value::Bool(true));
|
||||
}
|
||||
let extensions = openai_thinking_extensions(extensions);
|
||||
blocks.push(CanonicalContentBlock::Thinking {
|
||||
text: text.to_string(),
|
||||
signature: part_object
|
||||
@@ -1588,11 +1591,15 @@ pub(crate) fn openai_reasoning_blocks(message: &Map<String, Value>) -> Vec<Canon
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let extensions = openai_thinking_extensions(openai_extensions(
|
||||
part_object,
|
||||
&["type", "data"],
|
||||
));
|
||||
blocks.push(CanonicalContentBlock::Thinking {
|
||||
text: String::new(),
|
||||
signature: None,
|
||||
encrypted_content: Some(data.to_string()),
|
||||
extensions: openai_extensions(part_object, &["type", "data"]),
|
||||
extensions,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1840,6 +1847,7 @@ fn openai_responses_reasoning_block(text: String) -> CanonicalContentBlock {
|
||||
let mut extensions = BTreeMap::new();
|
||||
canonical_extension_object_mut(&mut extensions, "openai")
|
||||
.insert("omit_reasoning_parts".to_string(), Value::Bool(true));
|
||||
let extensions = openai_thinking_extensions(extensions);
|
||||
CanonicalContentBlock::Thinking {
|
||||
text,
|
||||
signature: None,
|
||||
@@ -1942,6 +1950,11 @@ pub(crate) fn openai_responses_output_to_canonical_blocks(
|
||||
}
|
||||
"reasoning" => {
|
||||
let mut emitted = false;
|
||||
let encrypted_content = item_object
|
||||
.get("encrypted_content")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
if let Some(summary_items) = item_object.get("summary").and_then(Value::as_array) {
|
||||
for summary in summary_items {
|
||||
let Some(summary_object) = summary.as_object() else {
|
||||
@@ -1960,18 +1973,32 @@ pub(crate) fn openai_responses_output_to_canonical_blocks(
|
||||
);
|
||||
canonical_extension_object_mut(&mut extensions, "openai")
|
||||
.insert("omit_reasoning_parts".to_string(), Value::Bool(true));
|
||||
let extensions = openai_thinking_extensions(extensions);
|
||||
blocks.push(CanonicalContentBlock::Thinking {
|
||||
text: text.to_string(),
|
||||
signature: None,
|
||||
encrypted_content: item_object
|
||||
.get("encrypted_content")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
extensions,
|
||||
});
|
||||
emitted = true;
|
||||
}
|
||||
}
|
||||
if !emitted && encrypted_content.is_some() {
|
||||
let mut extensions = openai_responses_extensions(
|
||||
item_object,
|
||||
&["type", "id", "status", "summary", "encrypted_content"],
|
||||
);
|
||||
canonical_extension_object_mut(&mut extensions, "openai")
|
||||
.insert("omit_reasoning_parts".to_string(), Value::Bool(true));
|
||||
let extensions = openai_thinking_extensions(extensions);
|
||||
blocks.push(CanonicalContentBlock::Thinking {
|
||||
text: String::new(),
|
||||
signature: None,
|
||||
encrypted_content,
|
||||
extensions,
|
||||
});
|
||||
emitted = true;
|
||||
}
|
||||
if !emitted {
|
||||
blocks.push(CanonicalContentBlock::Unknown {
|
||||
raw_type: item_type,
|
||||
@@ -2198,7 +2225,7 @@ pub(crate) fn openai_responses_part_to_canonical_block(
|
||||
);
|
||||
canonical_extension_object_mut(&mut extensions, "openai")
|
||||
.insert("omit_reasoning_parts".to_string(), Value::Bool(true));
|
||||
extensions
|
||||
openai_thinking_extensions(extensions)
|
||||
},
|
||||
}),
|
||||
"input_image" | "output_image" | "image_url" => {
|
||||
@@ -2659,6 +2686,14 @@ fn claude_thinking_extensions(mut extensions: BTreeMap<String, Value>) -> BTreeM
|
||||
extensions
|
||||
}
|
||||
|
||||
fn openai_thinking_extensions(mut extensions: BTreeMap<String, Value>) -> BTreeMap<String, Value> {
|
||||
canonical_extension_object_mut(&mut extensions, AETHER_EXTENSION_NAMESPACE).insert(
|
||||
"source".to_string(),
|
||||
Value::String(OPENAI_THINKING_SOURCE_MARKER.to_string()),
|
||||
);
|
||||
extensions
|
||||
}
|
||||
|
||||
pub(crate) fn is_claude_thinking_block(extensions: &BTreeMap<String, Value>) -> bool {
|
||||
extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
@@ -2667,6 +2702,14 @@ pub(crate) fn is_claude_thinking_block(extensions: &BTreeMap<String, Value>) ->
|
||||
== Some(CLAUDE_THINKING_SOURCE_MARKER)
|
||||
}
|
||||
|
||||
pub(crate) fn is_openai_thinking_block(extensions: &BTreeMap<String, Value>) -> bool {
|
||||
extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
.and_then(|value| value.get("source"))
|
||||
.and_then(Value::as_str)
|
||||
== Some(OPENAI_THINKING_SOURCE_MARKER)
|
||||
}
|
||||
|
||||
pub(crate) fn is_claude_tool_result(extensions: &BTreeMap<String, Value>) -> bool {
|
||||
extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
@@ -4115,7 +4158,11 @@ pub(crate) fn canonical_block_to_claude(
|
||||
encrypted_content,
|
||||
extensions,
|
||||
} => {
|
||||
if let Some(data) = encrypted_content.as_ref().filter(|value| !value.is_empty()) {
|
||||
if let Some(data) = encrypted_content
|
||||
.as_ref()
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|_| is_claude_thinking_block(extensions))
|
||||
{
|
||||
let mut out = Map::new();
|
||||
out.insert(
|
||||
"type".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user