mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
fix(ai): sanitize empty Read pages for Claude tools
This commit is contained in:
@@ -3825,6 +3825,7 @@ pub(crate) fn canonical_block_to_claude(
|
||||
input,
|
||||
extensions,
|
||||
} => {
|
||||
let input = claude_tool_use_input_without_empty_read_pages(name, input);
|
||||
let mut out = Map::new();
|
||||
out.insert("type".to_string(), Value::String("tool_use".to_string()));
|
||||
out.insert(
|
||||
@@ -3832,7 +3833,7 @@ pub(crate) fn canonical_block_to_claude(
|
||||
Value::String(claude_compatible_tool_use_id(id)),
|
||||
);
|
||||
out.insert("name".to_string(), Value::String(name.clone()));
|
||||
out.insert("input".to_string(), input.clone());
|
||||
out.insert("input".to_string(), input);
|
||||
out.extend(namespace_extension_object(extensions, "claude", &out));
|
||||
Some(Some(Value::Object(out)))
|
||||
}
|
||||
@@ -3867,6 +3868,18 @@ pub(crate) fn canonical_block_to_claude(
|
||||
}
|
||||
}
|
||||
|
||||
fn claude_tool_use_input_without_empty_read_pages(name: &str, input: &Value) -> Value {
|
||||
if name != "Read" || input.get("pages").and_then(Value::as_str) != Some("") {
|
||||
return input.clone();
|
||||
}
|
||||
let Some(object) = input.as_object() else {
|
||||
return input.clone();
|
||||
};
|
||||
let mut object = object.clone();
|
||||
object.remove("pages");
|
||||
Value::Object(object)
|
||||
}
|
||||
|
||||
fn canonical_tool_result_content_to_claude(
|
||||
output: Option<&Value>,
|
||||
content_text: Option<&str>,
|
||||
@@ -5596,6 +5609,74 @@ mod tests {
|
||||
assert_eq!(rebuilt["service_tier"], "flex");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_to_claude_response_drops_empty_pages_only_for_read_tool() {
|
||||
let response = json!({
|
||||
"id": "resp_read_pages",
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"model": "gpt-5.5",
|
||||
"output": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "call_read",
|
||||
"call_id": "call_read",
|
||||
"name": "Read",
|
||||
"arguments": "{\"file_path\":\"/tmp/a.txt\",\"offset\":0,\"limit\":20,\"pages\":\"\"}"
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "call_search",
|
||||
"call_id": "call_search",
|
||||
"name": "Search",
|
||||
"arguments": "{\"query\":\"\",\"pages\":\"\"}"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"input_tokens": 1,
|
||||
"output_tokens": 1,
|
||||
"total_tokens": 2
|
||||
}
|
||||
});
|
||||
|
||||
let canonical =
|
||||
from_openai_responses_to_canonical_response(&response).expect("canonical response");
|
||||
let claude = canonical_to_claude_response(&canonical);
|
||||
|
||||
assert_eq!(
|
||||
claude["content"][0]["input"],
|
||||
json!({
|
||||
"file_path": "/tmp/a.txt",
|
||||
"offset": 0,
|
||||
"limit": 20,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
claude["content"][1]["input"],
|
||||
json!({
|
||||
"query": "",
|
||||
"pages": "",
|
||||
})
|
||||
);
|
||||
|
||||
let rebuilt_responses = canonical_to_openai_responses_response(&canonical, &json!({}));
|
||||
let read_arguments = serde_json::from_str::<Value>(
|
||||
rebuilt_responses["output"][0]["arguments"]
|
||||
.as_str()
|
||||
.expect("arguments should be a string"),
|
||||
)
|
||||
.expect("arguments should be json");
|
||||
assert_eq!(
|
||||
read_arguments,
|
||||
json!({
|
||||
"file_path": "/tmp/a.txt",
|
||||
"offset": 0,
|
||||
"limit": 20,
|
||||
"pages": "",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_image_generation_call_becomes_canonical_image_block() {
|
||||
let response = json!({
|
||||
|
||||
Reference in New Issue
Block a user