diff --git a/lib/llm/src/protocols/openai/responses/mod.rs b/lib/llm/src/protocols/openai/responses/mod.rs
index f3334a460663..f58dcdfd0724 100644
--- a/lib/llm/src/protocols/openai/responses/mod.rs
+++ b/lib/llm/src/protocols/openai/responses/mod.rs
@@ -1228,7 +1228,24 @@ pub fn chat_completion_to_response(
if let Some(content_text) = content_text
&& !content_text.is_empty()
{
- let parsed_calls = parse_tool_call_text(&content_text);
+ let has_enabled_tools = params.tools.as_ref().is_some_and(|tools| {
+ tools.iter().any(|tool| match tool {
+ Tool::Function(_) => true,
+ Tool::Namespace(namespace) => namespace
+ .tools
+ .iter()
+ .any(|tool| matches!(tool, NamespaceToolParamTool::Function(_))),
+ _ => false,
+ })
+ }) && !matches!(
+ params.tool_choice,
+ Some(ToolChoiceParam::Mode(ToolChoiceOptions::None))
+ );
+ let parsed_calls = if has_enabled_tools {
+ parse_tool_call_text(&content_text)
+ } else {
+ Vec::new()
+ };
if !parsed_calls.is_empty() {
for (name, arguments) in parsed_calls {
let namespace = params.namespace_for_function(&name);
@@ -3048,6 +3065,104 @@ Let me check the weather.
assert!(calls.is_empty());
}
+ #[test]
+ fn test_text_tool_calls_preserved_when_tools_are_disabled() {
+ let text = r#"Example: reasoning{"name":"get_weather","arguments":{"city":"Beijing"}}"#;
+ let tools = vec![
+ serde_json::from_value(serde_json::json!({
+ "type": "function", "name": "get_weather", "parameters": {"type": "object"}
+ }))
+ .unwrap(),
+ ];
+ for (tools, tool_choice) in [
+ (
+ Some(tools),
+ Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
+ ),
+ (None, None),
+ (
+ Some(serde_json::from_value(serde_json::json!([{
+ "type": "namespace", "name": "weather", "description": "Weather tools", "tools": []
+ }])).unwrap()),
+ Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
+ ),
+ ] {
+ let params = ResponseParams {
+ tools,
+ tool_choice,
+ ..Default::default()
+ };
+ let response =
+ chat_completion_to_response(make_chat_resp_with_text(text), ¶ms, None).unwrap();
+ assert_eq!(response.inner.output.len(), 1);
+ let OutputItem::Message(message) = &response.inner.output[0] else {
+ panic!("tool-disabled request must preserve literal tool-call text");
+ };
+ let OutputMessageContent::OutputText(content) = &message.content[0] else {
+ panic!("expected output text");
+ };
+ assert_eq!(content.text, text);
+ }
+ }
+
+ #[test]
+ fn test_text_tool_calls_parsed_when_tools_are_enabled() {
+ let text =
+ r#"{"name":"get_weather","arguments":{"city":"Beijing"}}"#;
+ let tools = serde_json::from_value(serde_json::json!([{
+ "type": "function", "name": "get_weather", "parameters": {"type": "object"}
+ }]))
+ .unwrap();
+ let params = ResponseParams {
+ tools: Some(tools),
+ tool_choice: Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
+ ..Default::default()
+ };
+ let response =
+ chat_completion_to_response(make_chat_resp_with_text(text), ¶ms, None).unwrap();
+ assert_eq!(response.inner.output.len(), 1);
+ let OutputItem::FunctionCall(call) = &response.inner.output[0] else {
+ panic!("expected a function call for an enabled tool request");
+ };
+ assert_eq!(call.name, "get_weather");
+ assert_eq!(
+ serde_json::from_str::(&call.arguments).unwrap(),
+ serde_json::json!({"city":"Beijing"})
+ );
+ }
+
+ #[test]
+ fn test_text_tool_call_preserves_namespace() {
+ let params = ResponseParams {
+ tools: Some(
+ serde_json::from_value(serde_json::json!([{
+ "type": "namespace",
+ "name": "weather",
+ "description": "Weather tools",
+ "tools": [{"type": "function", "name": "get_weather"}]
+ }]))
+ .unwrap(),
+ ),
+ tool_choice: Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
+ ..Default::default()
+ };
+ let response = chat_completion_to_response(
+ make_chat_resp_with_text(
+ r#"{"name":"get_weather","arguments":{}}"#,
+ ),
+ ¶ms,
+ None,
+ )
+ .unwrap();
+ assert_eq!(response.inner.output.len(), 1);
+ let OutputItem::FunctionCall(call) = &response.inner.output[0] else {
+ panic!("expected a namespaced function call");
+ };
+ assert_eq!(call.name, "get_weather");
+ assert_eq!(call.namespace.as_deref(), Some("weather"));
+ assert_eq!(call.arguments, "{}");
+ }
+
#[test]
fn test_strip_tool_call_text() {
let text = r#"