mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-13 14:40:20 +08:00
fix(provider): preserve openai responses tool history in chat conversion
This commit is contained in:
@@ -163,6 +163,7 @@ mod tests {
|
|||||||
convert_openai_chat_request_to_claude_request,
|
convert_openai_chat_request_to_claude_request,
|
||||||
convert_openai_chat_request_to_openai_responses_request,
|
convert_openai_chat_request_to_openai_responses_request,
|
||||||
normalize_claude_request_to_openai_chat_request,
|
normalize_claude_request_to_openai_chat_request,
|
||||||
|
normalize_openai_responses_request_to_openai_chat_request,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -216,6 +217,64 @@ mod tests {
|
|||||||
assert_eq!(converted["messages"][0]["content"], "hello");
|
assert_eq!(converted["messages"][0]["content"], "hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_request_normalizer_keeps_tool_history_chat_safe() {
|
||||||
|
let call_id = "call_weather_123";
|
||||||
|
let tool_output = json!({
|
||||||
|
"toolCallId": call_id,
|
||||||
|
"input": {"city": "Hangzhou"},
|
||||||
|
"output": {
|
||||||
|
"content": [{"type": "text", "text": "sunny"}],
|
||||||
|
"isError": false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let body = json!({
|
||||||
|
"model": "glm-5.1",
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [{"type": "input_text", "text": "weather now"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_call",
|
||||||
|
"call_id": call_id,
|
||||||
|
"id": call_id,
|
||||||
|
"name": "mcp__mapsWeather",
|
||||||
|
"arguments": "{\"city\":\"Hangzhou\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_call_output",
|
||||||
|
"call_id": call_id,
|
||||||
|
"output": tool_output.to_string()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let converted = normalize_openai_responses_request_to_openai_chat_request(&body)
|
||||||
|
.expect("openai chat request");
|
||||||
|
let messages = converted["messages"].as_array().expect("messages");
|
||||||
|
|
||||||
|
assert_eq!(messages.len(), 3);
|
||||||
|
assert_eq!(messages[0]["role"], "user");
|
||||||
|
assert_eq!(messages[0]["content"], "weather now");
|
||||||
|
assert_eq!(messages[1]["role"], "assistant");
|
||||||
|
assert!(messages[1]["content"].is_null());
|
||||||
|
assert_eq!(messages[1]["tool_calls"][0]["id"], call_id);
|
||||||
|
assert_eq!(
|
||||||
|
messages[1]["tool_calls"][0]["function"]["name"],
|
||||||
|
"mcp__mapsWeather"
|
||||||
|
);
|
||||||
|
assert_eq!(messages[2]["role"], "tool");
|
||||||
|
assert_eq!(messages[2]["tool_call_id"], call_id);
|
||||||
|
let content = messages[2]["content"]
|
||||||
|
.as_str()
|
||||||
|
.expect("tool result content should stay a string");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<Value>(content).expect("tool output json"),
|
||||||
|
tool_output
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_normalizer_preserves_multiple_claude_tool_results() {
|
fn request_normalizer_preserves_multiple_claude_tool_results() {
|
||||||
let body = json!({
|
let body = json!({
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub(crate) const OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE: &str = "openai_cli
|
|||||||
const AETHER_EXTENSION_NAMESPACE: &str = "aether";
|
const AETHER_EXTENSION_NAMESPACE: &str = "aether";
|
||||||
const CLAUDE_TOOL_RESULT_SOURCE_MARKER: &str = "claude_tool_result";
|
const CLAUDE_TOOL_RESULT_SOURCE_MARKER: &str = "claude_tool_result";
|
||||||
const OPENAI_CHAT_TOOL_RESULT_SOURCE_MARKER: &str = "openai_chat_tool_result";
|
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]";
|
const OPENAI_CHAT_TOOL_ERROR_PREFIX: &str = "[tool error]";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -1644,6 +1645,21 @@ pub(crate) fn openai_responses_input_to_canonical_messages(
|
|||||||
});
|
});
|
||||||
let raw_output = item_object.get("output");
|
let raw_output = item_object.get("output");
|
||||||
let output = Some(parse_jsonish_value(raw_output));
|
let output = Some(parse_jsonish_value(raw_output));
|
||||||
|
let mut extensions = openai_responses_extensions(
|
||||||
|
item_object,
|
||||||
|
&[
|
||||||
|
"type",
|
||||||
|
"call_id",
|
||||||
|
"tool_call_id",
|
||||||
|
"id",
|
||||||
|
"output",
|
||||||
|
"is_error",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
extensions.insert(
|
||||||
|
AETHER_EXTENSION_NAMESPACE.to_string(),
|
||||||
|
json!({ "source": OPENAI_RESPONSES_TOOL_RESULT_SOURCE_MARKER }),
|
||||||
|
);
|
||||||
messages.push(CanonicalMessage {
|
messages.push(CanonicalMessage {
|
||||||
role: CanonicalRole::Tool,
|
role: CanonicalRole::Tool,
|
||||||
content: vec![CanonicalContentBlock::ToolResult {
|
content: vec![CanonicalContentBlock::ToolResult {
|
||||||
@@ -1655,17 +1671,7 @@ pub(crate) fn openai_responses_input_to_canonical_messages(
|
|||||||
.get("is_error")
|
.get("is_error")
|
||||||
.and_then(Value::as_bool)
|
.and_then(Value::as_bool)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
extensions: openai_responses_extensions(
|
extensions,
|
||||||
item_object,
|
|
||||||
&[
|
|
||||||
"type",
|
|
||||||
"call_id",
|
|
||||||
"tool_call_id",
|
|
||||||
"id",
|
|
||||||
"output",
|
|
||||||
"is_error",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
}],
|
}],
|
||||||
extensions: BTreeMap::new(),
|
extensions: BTreeMap::new(),
|
||||||
});
|
});
|
||||||
@@ -1836,6 +1842,21 @@ pub(crate) fn openai_responses_output_to_canonical_blocks(
|
|||||||
.unwrap_or_else(|| format!("call_auto_{index}"));
|
.unwrap_or_else(|| format!("call_auto_{index}"));
|
||||||
let raw_output = item_object.get("output");
|
let raw_output = item_object.get("output");
|
||||||
let output = Some(parse_jsonish_value(raw_output));
|
let output = Some(parse_jsonish_value(raw_output));
|
||||||
|
let mut extensions = openai_responses_extensions(
|
||||||
|
item_object,
|
||||||
|
&[
|
||||||
|
"type",
|
||||||
|
"id",
|
||||||
|
"call_id",
|
||||||
|
"tool_call_id",
|
||||||
|
"output",
|
||||||
|
"is_error",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
extensions.insert(
|
||||||
|
AETHER_EXTENSION_NAMESPACE.to_string(),
|
||||||
|
json!({ "source": OPENAI_RESPONSES_TOOL_RESULT_SOURCE_MARKER }),
|
||||||
|
);
|
||||||
blocks.push(CanonicalContentBlock::ToolResult {
|
blocks.push(CanonicalContentBlock::ToolResult {
|
||||||
tool_use_id: id,
|
tool_use_id: id,
|
||||||
name: None,
|
name: None,
|
||||||
@@ -1845,17 +1866,7 @@ pub(crate) fn openai_responses_output_to_canonical_blocks(
|
|||||||
.get("is_error")
|
.get("is_error")
|
||||||
.and_then(Value::as_bool)
|
.and_then(Value::as_bool)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
extensions: openai_responses_extensions(
|
extensions,
|
||||||
item_object,
|
|
||||||
&[
|
|
||||||
"type",
|
|
||||||
"id",
|
|
||||||
"call_id",
|
|
||||||
"tool_call_id",
|
|
||||||
"output",
|
|
||||||
"is_error",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"image_generation_call" => {
|
"image_generation_call" => {
|
||||||
@@ -2381,11 +2392,7 @@ fn canonical_message_blocks_to_openai_chat(
|
|||||||
output.insert(
|
output.insert(
|
||||||
"content".to_string(),
|
"content".to_string(),
|
||||||
if !tool_calls.is_empty() && content_parts.is_empty() {
|
if !tool_calls.is_empty() && content_parts.is_empty() {
|
||||||
if reasoning_parts.is_empty() {
|
Value::Null
|
||||||
Value::Array(Vec::new())
|
|
||||||
} else {
|
|
||||||
Value::Null
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
openai_content_value_from_parts(content_parts, false)
|
openai_content_value_from_parts(content_parts, false)
|
||||||
},
|
},
|
||||||
@@ -2432,6 +2439,11 @@ fn canonical_tool_result_to_openai_chat(block: &CanonicalContentBlock) -> Value
|
|||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
} else if is_openai_responses_tool_result(extensions) {
|
||||||
|
openai_responses_tool_result_content_for_chat(
|
||||||
|
result_output.as_ref(),
|
||||||
|
content_text.as_deref(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
result_output
|
result_output
|
||||||
.clone()
|
.clone()
|
||||||
@@ -2449,6 +2461,28 @@ fn is_claude_tool_result(extensions: &BTreeMap<String, Value>) -> bool {
|
|||||||
== Some(CLAUDE_TOOL_RESULT_SOURCE_MARKER)
|
== Some(CLAUDE_TOOL_RESULT_SOURCE_MARKER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_openai_responses_tool_result(extensions: &BTreeMap<String, Value>) -> bool {
|
||||||
|
extensions
|
||||||
|
.get(AETHER_EXTENSION_NAMESPACE)
|
||||||
|
.and_then(|value| value.get("source"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
== Some(OPENAI_RESPONSES_TOOL_RESULT_SOURCE_MARKER)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openai_responses_tool_result_content_for_chat(
|
||||||
|
output: Option<&Value>,
|
||||||
|
content_text: Option<&str>,
|
||||||
|
) -> Value {
|
||||||
|
if let Some(text) = content_text {
|
||||||
|
return Value::String(text.to_string());
|
||||||
|
}
|
||||||
|
match output {
|
||||||
|
Some(Value::String(text)) => Value::String(text.clone()),
|
||||||
|
Some(value) => Value::String(value.to_string()),
|
||||||
|
None => Value::String(String::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn openai_chat_tool_result_content(output: Option<&Value>, content_text: Option<&str>) -> Value {
|
fn openai_chat_tool_result_content(output: Option<&Value>, content_text: Option<&str>) -> Value {
|
||||||
match output {
|
match output {
|
||||||
Some(Value::String(text)) => Value::String(text.clone()),
|
Some(Value::String(text)) => Value::String(text.clone()),
|
||||||
|
|||||||
Reference in New Issue
Block a user