From c35fd78411adf4659197db1add764b9ad9925398 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 23 Jul 2025 12:57:06 -0400 Subject: [PATCH 1/5] Add a test that just prints --- crates/goose/src/providers/formats/openai.rs | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 83d7ac29980f..63e09f9fed84 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -609,6 +609,8 @@ pub fn create_request( mod tests { use super::*; use serde_json::json; + use tokio::pin; + use tokio_stream::{self, StreamExt}; #[test] fn test_validate_tool_schemas() { @@ -1096,4 +1098,40 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_streamed_multi_tool_response_to_messages() -> anyhow::Result<()> { + let response_lines = r#" +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":"I'll run both"},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288340} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":" `ls` commands in a"},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288340} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":" single turn for you -"},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288340} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":" one on the current directory an"},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288340} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":"d one on the `working_dir`."},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288340} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":1,"id":"toolu_bdrk_01RMTd7R9DzQjEEWgDwzcBsU","type":"function","function":{"name":"developer__shell","arguments":""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":1,"function":{"arguments":""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":1,"function":{"arguments":"{\""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":1,"function":{"arguments":"command\": \"l"}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":1,"function":{"arguments":"s\"}"}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"id":"toolu_bdrk_016bgVTGZdpjP8ehjMWp9cWW","type":"function","function":{"name":"developer__shell","arguments":""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"function":{"arguments":""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288341} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"function":{"arguments":"{\""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288342} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"function":{"arguments":"command\""}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288342} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"function":{"arguments":": \"ls wor"}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288342} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"function":{"arguments":"king_dir"}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288342} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":2,"function":{"arguments":"\"}"}}]},"index":0,"finish_reason":null}],"usage":{"prompt_tokens":4982,"completion_tokens":null,"total_tokens":null},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288342} +data: {"model":"us.anthropic.claude-sonnet-4-20250514-v1:0","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":4982,"completion_tokens":122,"total_tokens":5104},"object":"chat.completion.chunk","id":"msg_bdrk_014pifLTHsNZz6Lmtw1ywgDJ","created":1753288342} +data: [DONE] +"#; + + let response_stream = + tokio_stream::iter(response_lines.lines().map(|line| Ok(line.to_string()))); + let messages = response_to_streaming_message(response_stream); + pin!(messages); + + while let Some(Ok((message, _usage))) = messages.next().await { + println!("Message: {:?}", message); + } + + Ok(()) + } } From 70ea869081625bde271b2494b42db2444865b68c Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 23 Jul 2025 13:02:27 -0400 Subject: [PATCH 2/5] Make the test fail --- crates/goose/src/providers/formats/openai.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 63e09f9fed84..f6c60e147f64 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -1129,9 +1129,22 @@ data: [DONE] pin!(messages); while let Some(Ok((message, _usage))) = messages.next().await { - println!("Message: {:?}", message); + if let Some(msg) = message { + if msg.content.len() == 2 { + if let (MessageContent::ToolRequest(req1), MessageContent::ToolRequest(req2)) = + (&msg.content[0], &msg.content[1]) + { + if req1.tool_call.is_ok() && req2.tool_call.is_ok() { + // We expect two tool calls in the response + assert_eq!(req1.tool_call.as_ref().unwrap().name, "developer__shell"); + assert_eq!(req2.tool_call.as_ref().unwrap().name, "developer__shell"); + return Ok(()); + } + } + } + } } - Ok(()) + panic!("Expected tool call message with two calls, but did not see it"); } } From db934c4ca09a238d7ad1530da338ef404745b698 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 23 Jul 2025 13:13:31 -0400 Subject: [PATCH 3/5] Claude's fix --- crates/goose/src/providers/formats/openai.rs | 111 ++++++++++++------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index f6c60e147f64..26c1f9f003b7 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -437,56 +437,91 @@ where if chunk.choices.is_empty() { yield (None, usage) } else if let Some(tool_calls) = &chunk.choices[0].delta.tool_calls { - let tool_call = &tool_calls[0]; - let id = tool_call.id.clone().ok_or(anyhow!("No tool call ID"))?; - let function_name = tool_call.function.name.clone().ok_or(anyhow!("No function name"))?; - let mut arguments = tool_call.function.arguments.clone(); - - while let Some(response_chunk) = stream.next().await { - if response_chunk.as_ref().is_ok_and(|s| s == "data: [DONE]") { - break 'outer; + // Initialize tracking for multiple tool calls + let mut tool_call_data: std::collections::HashMap = std::collections::HashMap::new(); + + // Process initial tool call(s) + for tool_call in tool_calls { + if let (Some(index), Some(id), Some(name)) = (tool_call.index, &tool_call.id, &tool_call.function.name) { + tool_call_data.insert(index, (id.clone(), name.clone(), tool_call.function.arguments.clone())); } - let response_str = response_chunk?; - if let Some(line) = strip_data_prefix(&response_str) { - let tool_chunk: StreamingChunk = serde_json::from_str(line) - .map_err(|e| anyhow!("Failed to parse streaming chunk: {}: {:?}", e, &line))?; - let more_args = tool_chunk.choices[0].delta.tool_calls.as_ref() - .and_then(|calls| calls.first()) - .map(|call| call.function.arguments.as_str()); - if let Some(more_args) = more_args { - arguments.push_str(more_args); - } else { - break; + } + + // Continue collecting tool call arguments until we don't see more tool calls + let mut done = false; + while !done { + if let Some(response_chunk) = stream.next().await { + if response_chunk.as_ref().is_ok_and(|s| s == "data: [DONE]") { + break 'outer; + } + let response_str = response_chunk?; + if let Some(line) = strip_data_prefix(&response_str) { + let tool_chunk: StreamingChunk = serde_json::from_str(line) + .map_err(|e| anyhow!("Failed to parse streaming chunk: {}: {:?}", e, &line))?; + + if let Some(delta_tool_calls) = &tool_chunk.choices[0].delta.tool_calls { + for delta_call in delta_tool_calls { + if let Some(index) = delta_call.index { + if let Some((_, _, ref mut args)) = tool_call_data.get_mut(&index) { + args.push_str(&delta_call.function.arguments); + } else if let (Some(id), Some(name)) = (&delta_call.id, &delta_call.function.name) { + // New tool call starting + tool_call_data.insert(index, (id.clone(), name.clone(), delta_call.function.arguments.clone())); + } + } + } + } else { + // No more tool calls in this chunk, we're done collecting + done = true; + } + + // Check if this chunk indicates the end of tool calls + if tool_chunk.choices[0].finish_reason == Some("tool_calls".to_string()) { + done = true; + } } + } else { + // Stream ended + break; } } - let parsed = if arguments.is_empty() { - Ok(json!({})) - } else { - serde_json::from_str::(&arguments) - }; - - let content = match parsed { - Ok(params) => MessageContent::tool_request( - id, - Ok(ToolCall::new(function_name, params)), - ), - Err(e) => { - let error = ToolError::InvalidParameters(format!( - "Could not interpret tool use parameters for id {}: {}", - id, e - )); - MessageContent::tool_request(id, Err(error)) + // Convert all collected tool calls to MessageContent + let mut contents = Vec::new(); + let mut sorted_indices: Vec<_> = tool_call_data.keys().cloned().collect(); + sorted_indices.sort(); + + for index in sorted_indices { + if let Some((id, function_name, arguments)) = tool_call_data.get(&index) { + let parsed = if arguments.is_empty() { + Ok(json!({})) + } else { + serde_json::from_str::(arguments) + }; + + let content = match parsed { + Ok(params) => MessageContent::tool_request( + id.clone(), + Ok(ToolCall::new(function_name.clone(), params)), + ), + Err(e) => { + let error = ToolError::InvalidParameters(format!( + "Could not interpret tool use parameters for id {}: {}", + id, e + )); + MessageContent::tool_request(id.clone(), Err(error)) + } + }; + contents.push(content); } - }; + } yield ( Some(Message { id: chunk.id, role: Role::Assistant, created: chrono::Utc::now().timestamp(), - content: vec![content], + content: contents, }), usage, ) From 58059c94332a3f57d139d43916c44cdc900d15b8 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 23 Jul 2025 13:16:36 -0400 Subject: [PATCH 4/5] Print them --- crates/goose/src/providers/formats/openai.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 26c1f9f003b7..cfc5afe978e7 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -1165,6 +1165,7 @@ data: [DONE] while let Some(Ok((message, _usage))) = messages.next().await { if let Some(msg) = message { + println!("{:?}", msg); if msg.content.len() == 2 { if let (MessageContent::ToolRequest(req1), MessageContent::ToolRequest(req2)) = (&msg.content[0], &msg.content[1]) From 5d87dbb8ef40ed05e73a111ccf032a126c67016d Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 23 Jul 2025 13:18:23 -0400 Subject: [PATCH 5/5] de-llmify comments --- crates/goose/src/providers/formats/openai.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index cfc5afe978e7..4e32706bfc73 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -437,17 +437,14 @@ where if chunk.choices.is_empty() { yield (None, usage) } else if let Some(tool_calls) = &chunk.choices[0].delta.tool_calls { - // Initialize tracking for multiple tool calls let mut tool_call_data: std::collections::HashMap = std::collections::HashMap::new(); - // Process initial tool call(s) for tool_call in tool_calls { if let (Some(index), Some(id), Some(name)) = (tool_call.index, &tool_call.id, &tool_call.function.name) { tool_call_data.insert(index, (id.clone(), name.clone(), tool_call.function.arguments.clone())); } } - // Continue collecting tool call arguments until we don't see more tool calls let mut done = false; while !done { if let Some(response_chunk) = stream.next().await { @@ -465,28 +462,23 @@ where if let Some((_, _, ref mut args)) = tool_call_data.get_mut(&index) { args.push_str(&delta_call.function.arguments); } else if let (Some(id), Some(name)) = (&delta_call.id, &delta_call.function.name) { - // New tool call starting tool_call_data.insert(index, (id.clone(), name.clone(), delta_call.function.arguments.clone())); } } } } else { - // No more tool calls in this chunk, we're done collecting done = true; } - // Check if this chunk indicates the end of tool calls if tool_chunk.choices[0].finish_reason == Some("tool_calls".to_string()) { done = true; } } } else { - // Stream ended break; } } - // Convert all collected tool calls to MessageContent let mut contents = Vec::new(); let mut sorted_indices: Vec<_> = tool_call_data.keys().cloned().collect(); sorted_indices.sort();