mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-10 01:54:49 +08:00
fix(api): omit tool_calls field from assistant messages when empty
When serializing a multi-turn conversation for the OpenAI-compatible path, assistant messages with no tool calls were always emitting 'tool_calls: []'. Some providers reject requests where a prior assistant turn carries an explicit empty tool_calls array (400 on subsequent turns after a plain text assistant response). Fix: only include 'tool_calls' in the serialized assistant message when the vec is non-empty. Empty case omits the field entirely. This is a companion fix tofd7aade(null tool_calls in stream delta). The two bugs are symmetric:fd7aadehandled inbound null -> empty vec; this handles outbound empty vec -> field omitted. Two regression tests added: - assistant_message_without_tool_calls_omits_tool_calls_field - assistant_message_with_tool_calls_includes_tool_calls_field 115 api tests pass. Fmt clean. Source: gaebal-gajae repro 2026-04-09 (400 on multi-turn, companion to null tool_calls stream-delta fix).
This commit is contained in:
@@ -853,11 +853,16 @@ fn translate_message(message: &InputMessage) -> Vec<Value> {
|
||||
if text.is_empty() && tool_calls.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![json!({
|
||||
let mut msg = serde_json::json!({
|
||||
"role": "assistant",
|
||||
"content": (!text.is_empty()).then_some(text),
|
||||
"tool_calls": tool_calls,
|
||||
})]
|
||||
});
|
||||
// Only include tool_calls when non-empty: some providers reject
|
||||
// assistant messages with an explicit empty tool_calls array.
|
||||
if !tool_calls.is_empty() {
|
||||
msg["tool_calls"] = json!(tool_calls);
|
||||
}
|
||||
vec![msg]
|
||||
}
|
||||
}
|
||||
_ => message
|
||||
@@ -1526,6 +1531,73 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Regression: when building a multi-turn request where a prior assistant
|
||||
/// turn has no tool calls, the serialized assistant message must NOT include
|
||||
/// `tool_calls: []`. Some providers reject requests that carry an empty
|
||||
/// tool_calls array on assistant turns (gaebal-gajae repro 2026-04-09).
|
||||
#[test]
|
||||
fn assistant_message_without_tool_calls_omits_tool_calls_field() {
|
||||
use crate::types::{InputContentBlock, InputMessage};
|
||||
|
||||
let request = MessageRequest {
|
||||
model: "gpt-4o".to_string(),
|
||||
max_tokens: 100,
|
||||
messages: vec![InputMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: vec![InputContentBlock::Text {
|
||||
text: "Hello".to_string(),
|
||||
}],
|
||||
}],
|
||||
stream: false,
|
||||
..Default::default()
|
||||
};
|
||||
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||
let messages = payload["messages"].as_array().unwrap();
|
||||
let assistant_msg = messages
|
||||
.iter()
|
||||
.find(|m| m["role"] == "assistant")
|
||||
.expect("assistant message must be present");
|
||||
assert!(
|
||||
assistant_msg.get("tool_calls").is_none(),
|
||||
"assistant message without tool calls must omit tool_calls field: {:?}",
|
||||
assistant_msg
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: assistant messages WITH tool calls must still include
|
||||
/// the tool_calls array (normal multi-turn tool-use flow).
|
||||
#[test]
|
||||
fn assistant_message_with_tool_calls_includes_tool_calls_field() {
|
||||
use crate::types::{InputContentBlock, InputMessage};
|
||||
|
||||
let request = MessageRequest {
|
||||
model: "gpt-4o".to_string(),
|
||||
max_tokens: 100,
|
||||
messages: vec![InputMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: vec![InputContentBlock::ToolUse {
|
||||
id: "call_1".to_string(),
|
||||
name: "read_file".to_string(),
|
||||
input: serde_json::json!({"path": "/tmp/test"}),
|
||||
}],
|
||||
}],
|
||||
stream: false,
|
||||
..Default::default()
|
||||
};
|
||||
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||
let messages = payload["messages"].as_array().unwrap();
|
||||
let assistant_msg = messages
|
||||
.iter()
|
||||
.find(|m| m["role"] == "assistant")
|
||||
.expect("assistant message must be present");
|
||||
let tool_calls = assistant_msg
|
||||
.get("tool_calls")
|
||||
.expect("assistant message with tool calls must include tool_calls field");
|
||||
assert!(tool_calls.is_array());
|
||||
assert_eq!(tool_calls.as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_gpt5_uses_max_tokens() {
|
||||
// Older OpenAI models expect `max_tokens`; verify gpt-4o is unaffected.
|
||||
|
||||
Reference in New Issue
Block a user