Skip to content
47 changes: 39 additions & 8 deletions src/llm/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ impl SpacebotModel {
self
}

/// Check if this model requires reasoning_content field for tool calls.
fn needs_reasoning_content(&self) -> bool {
let lower = self.model_name.to_lowercase();
lower.contains("k2.5")
|| lower.contains("k2-5")
|| lower.contains("thinking")
|| lower.contains("reasoning")
}

/// Attach a worker type label for metrics (e.g. "builtin", "opencode").
pub fn with_worker_type(mut self, worker_type: impl Into<String>) -> Self {
self.worker_type = Some(worker_type.into());
Expand Down Expand Up @@ -839,7 +848,11 @@ impl SpacebotModel {
}));
}

messages.extend(convert_messages_to_openai(&request.chat_history));
let needs_reasoning = self.needs_reasoning_content();
messages.extend(convert_messages_to_openai(
&request.chat_history,
needs_reasoning,
));

let api_model_name = self.remap_model_name_for_api();
let mut body = serde_json::json!({
Expand Down Expand Up @@ -1386,7 +1399,11 @@ impl SpacebotModel {
}));
}

messages.extend(convert_messages_to_openai(&request.chat_history));
let needs_reasoning = self.needs_reasoning_content();
messages.extend(convert_messages_to_openai(
&request.chat_history,
needs_reasoning,
));

let mut body = serde_json::json!({
"model": self.model_name,
Expand Down Expand Up @@ -1478,7 +1495,11 @@ impl SpacebotModel {
}));
}

messages.extend(convert_messages_to_openai(&request.chat_history));
let needs_reasoning = self.needs_reasoning_content();
messages.extend(convert_messages_to_openai(
&request.chat_history,
needs_reasoning,
));

let api_model_name = self.remap_model_name_for_api();
let mut body = serde_json::json!({
Expand Down Expand Up @@ -1826,7 +1847,10 @@ pub fn convert_messages_to_anthropic(messages: &OneOrMany<Message>) -> Vec<serde
.collect()
}

fn convert_messages_to_openai(messages: &OneOrMany<Message>) -> Vec<serde_json::Value> {
fn convert_messages_to_openai(
messages: &OneOrMany<Message>,
needs_reasoning_content: bool,
) -> Vec<serde_json::Value> {
let mut result = Vec::new();

for message in messages.iter() {
Expand Down Expand Up @@ -1939,6 +1963,13 @@ fn convert_messages_to_openai(messages: &OneOrMany<Message>) -> Vec<serde_json::
}
if !tool_calls.is_empty() {
msg["tool_calls"] = serde_json::json!(tool_calls);
// Reasoning-capable models (e.g., kimi-k2.5, kimi-k2.5-nvfp4) require the
// reasoning_content field when tool_calls are present and thinking is enabled.
// Only add it when needed, and don’t overwrite it if we ever start carrying it
// through in Message::Assistant.
if needs_reasoning_content && msg.get("reasoning_content").is_none() {
msg["reasoning_content"] = serde_json::json!(".");
}
}
result.push(msg);
}
Expand Down Expand Up @@ -3972,7 +4003,7 @@ mod tests {
let messages = OneOrMany::one(Message::System {
content: "You are a helpful assistant".to_string(),
});
let converted = convert_messages_to_openai(&messages);
let converted = convert_messages_to_openai(&messages, false);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0]["role"], "user");
assert_eq!(converted[0]["content"], "You are a helpful assistant");
Expand Down Expand Up @@ -4099,7 +4130,7 @@ mod tests {
.expect("non-empty assistant content"),
});

let converted = convert_messages_to_openai(&messages);
let converted = convert_messages_to_openai(&messages, false);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0]["role"], "assistant");
assert!(converted[0]["content"].is_null());
Expand Down Expand Up @@ -4145,7 +4176,7 @@ mod tests {
.expect("non-empty assistant content"),
});

let converted = convert_messages_to_openai(&messages);
let converted = convert_messages_to_openai(&messages, false);
assert_eq!(converted.len(), 1);
assert!(converted[0]["content"].is_null());
assert_eq!(converted[0]["reasoning_content"], "");
Expand Down Expand Up @@ -4199,7 +4230,7 @@ mod tests {
})),
});

let converted = convert_messages_to_openai(&messages);
let converted = convert_messages_to_openai(&messages, false);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0]["role"], "tool");
assert_eq!(converted[0]["tool_call_id"], "stable-call-id");
Expand Down
Loading