Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/goose/src/providers/formats/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::conversation::message::{Message, MessageContent};
use crate::model::ModelConfig;
use crate::providers::base::Usage;
use crate::providers::errors::ProviderError;
use crate::providers::utils::{convert_image, ImageFormat};
use anyhow::{anyhow, Result};
use rmcp::model::{object, CallToolRequestParam, ErrorCode, ErrorData, JsonObject, Role, Tool};
use rmcp::object as json_object;
Expand Down Expand Up @@ -106,7 +107,9 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
DATA_FIELD: redacted.data
}));
}
MessageContent::Image(_) => continue, // Anthropic doesn't support image content yet
MessageContent::Image(image) => {
content.push(convert_image(image, &ImageFormat::Anthropic));
}
MessageContent::FrontendToolRequest(tool_request) => {
if let Ok(tool_call) = &tool_request.tool_call {
content.push(json!({
Expand Down
42 changes: 31 additions & 11 deletions crates/goose/src/providers/formats/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,25 +63,22 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
});

let mut output = Vec::new();
let mut content_array = Vec::new();
let mut text_array = Vec::new();

for content in &message.content {
match content {
MessageContent::Text(text) => {
if !text.text.is_empty() {
// Check for image paths in the text
if let Some(image_path) = detect_image_path(&text.text) {
// Try to load and convert the image
if let Ok(image) = load_image_file(image_path) {
converted["content"] = json!([
{"type": "text", "text": text.text},
convert_image(&image, image_format)
]);
content_array.push(json!({"type": "text", "text": text.text}));
content_array.push(convert_image(&image, image_format));
} else {
// If image loading fails, just use the text
converted["content"] = json!(text.text);
text_array.push(text.text.clone());
}
} else {
converted["content"] = json!(text.text);
text_array.push(text.text.clone());
Comment on lines 72 to +81
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an image path is detected and loaded successfully, the text is added to content_array but subsequent non-image text goes to text_array. This will cause text_array content to be lost since line 243-246 only uses content_array when it's non-empty. All text should go to content_array when any images are present.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this legit feedback @alexhancock ? it sounds convincing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be slop as further down is consolidated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah below I do

        if !content_array.is_empty() {
            converted["content"] = json!(content_array);
        } else if !text_array.is_empty() {
            converted["content"] = json!(text_array.join("\n"));
        }

which seems right

}
}
}
Expand Down Expand Up @@ -205,8 +202,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
// Skip tool confirmation requests
}
MessageContent::Image(image) => {
// Handle direct image content
converted["content"] = json!([convert_image(image, image_format)]);
content_array.push(convert_image(image, image_format));
}
MessageContent::FrontendToolRequest(request) => match &request.tool_call {
Ok(tool_call) => {
Expand Down Expand Up @@ -244,9 +240,16 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
}
}

if !content_array.is_empty() {
converted["content"] = json!(content_array);
} else if !text_array.is_empty() {
converted["content"] = json!(text_array.join("\n"));
Comment on lines +243 to +246
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Text content is lost when both content_array and text_array have items. If a message has text, then an image, then more text, the text in text_array is discarded because only content_array is used. Fix: merge text_array into content_array before checking, e.g., if !text_array.is_empty() { content_array.push(json!({"type": "text", "text": text_array.join("\n")})); } before line 243.

Suggested change
if !content_array.is_empty() {
converted["content"] = json!(content_array);
} else if !text_array.is_empty() {
converted["content"] = json!(text_array.join("\n"));
// Merge text_array into content_array if text exists
if !text_array.is_empty() {
content_array.push(json!({"type": "text", "text": text_array.join("\n")}));
}
if !content_array.is_empty() {
converted["content"] = json!(content_array);

Copilot uses AI. Check for mistakes.
}

if converted.get("content").is_some() || converted.get("tool_calls").is_some() {
output.insert(0, converted);
}

messages_spec.extend(output);
}

Expand Down Expand Up @@ -1230,6 +1233,23 @@ mod tests {
Ok(())
}

#[test]
fn test_format_messages_multiple_text_blocks() -> anyhow::Result<()> {
let message = Message::user()
.with_text("--- Resource: file:///test.md ---\n# Test\n\n---\n")
.with_text(" What is in the file?");

let spec = format_messages(&[message], &ImageFormat::OpenAi);

assert_eq!(spec.len(), 1);
assert_eq!(spec[0]["role"], "user");
assert_eq!(
spec[0]["content"],
"--- Resource: file:///test.md ---\n# Test\n\n---\n\n What is in the file?"
);
Ok(())
}

#[test]
fn test_create_request_gpt_4o() -> anyhow::Result<()> {
// Test default medium reasoning effort for O3 model
Expand Down
40 changes: 30 additions & 10 deletions ui/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading