Skip to content

Commit 3566bab

Browse files
committed
fix: handle invalid image URLs in conversations and resumed sessions
1 parent 3448c50 commit 3566bab

5 files changed

Lines changed: 103 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to Sofos are documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
- Increase `MAX_TOOL_OUTPUT_TOKENS`
9+
10+
### Fixed
11+
- Gracefully handle invalid image URLs in conversations and resumed sessions
12+
713
## [0.1.19] - 2026-01-08
814

915
### Changed

src/repl/conversation.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ Show imperial units only when the user explicitly asks for them."#,
275275
self.trim_if_needed();
276276
}
277277

278+
/// Remove the last message from the conversation (used for error recovery)
279+
pub fn remove_last_message(&mut self) {
280+
self.messages.pop();
281+
}
282+
278283
pub fn _len(&self) -> usize {
279284
self.messages.len()
280285
}

src/repl/mod.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ impl Repl {
382382
});
383383

384384
let client = self.client.clone();
385+
let client_for_retry = client.clone();
385386
let req = initial_request;
386387
let mut request_handle = runtime.spawn(async move { client.create_message(req).await });
387388

@@ -408,7 +409,95 @@ impl Repl {
408409
return Ok(());
409410
}
410411

411-
let response = response_result?;
412+
// Handle API errors, especially those related to invalid images
413+
let response = match response_result {
414+
Ok(resp) => resp,
415+
Err(e) => {
416+
// Check if this is an image-related API error
417+
if let SofosError::Api(ref msg) = e {
418+
let is_400_error = msg.contains("400");
419+
let is_image_error = msg.contains("Unable to download")
420+
|| msg.contains("invalid_request_error")
421+
|| msg.contains("verify the URL");
422+
423+
// Check if current message OR conversation has images
424+
let current_has_images = !image_refs.is_empty();
425+
let conversation_has_images = self.session_state.conversation.messages()
426+
.iter()
427+
.any(|msg| {
428+
use crate::api::{MessageContent, MessageContentBlock};
429+
if let MessageContent::Blocks { content } = &msg.content {
430+
content.iter().any(|block| matches!(block, MessageContentBlock::Image { .. }))
431+
} else {
432+
false
433+
}
434+
});
435+
436+
let has_images = current_has_images || conversation_has_images;
437+
438+
if is_400_error && is_image_error && has_images {
439+
println!(
440+
"\n{} {}\n",
441+
"⚠️ Image loading error:".bright_yellow().bold(),
442+
"One or more image URLs in the conversation could not be loaded by the API"
443+
);
444+
445+
self.session_state.conversation.remove_last_message();
446+
447+
// Remove ALL images from conversation
448+
let messages = self.session_state.conversation.messages();
449+
let mut cleaned_messages = Vec::new();
450+
451+
for msg in messages {
452+
use crate::api::{Message, MessageContent, MessageContentBlock};
453+
let cleaned_msg = match &msg.content {
454+
MessageContent::Blocks { content } => {
455+
let filtered_blocks: Vec<MessageContentBlock> = content
456+
.iter()
457+
.filter(|block| !matches!(block, MessageContentBlock::Image { .. }))
458+
.cloned()
459+
.collect();
460+
461+
if filtered_blocks.is_empty() {
462+
continue;
463+
} else {
464+
Message {
465+
role: msg.role.clone(),
466+
content: MessageContent::Blocks { content: filtered_blocks },
467+
}
468+
}
469+
}
470+
_ => msg.clone(),
471+
};
472+
cleaned_messages.push(cleaned_msg);
473+
}
474+
475+
self.session_state.conversation.clear();
476+
self.session_state.conversation.restore_messages(cleaned_messages);
477+
478+
let error_message = if !image_refs.is_empty() {
479+
"[SYSTEM ERROR: Image URLs in your message could not be loaded and have been removed from the conversation.]"
480+
} else {
481+
"[SYSTEM ERROR: Image URLs from a previous message could not be loaded and have been removed from the conversation. You can continue normally.]"
482+
}.to_string();
483+
484+
self.session_state.conversation.add_user_message(error_message);
485+
let new_request = self.build_initial_request();
486+
487+
println!("{}", "Retrying request without images...".dimmed());
488+
println!();
489+
490+
runtime.block_on(async {
491+
client_for_retry.create_message(new_request).await
492+
})?
493+
} else {
494+
return Err(e);
495+
}
496+
} else {
497+
return Err(e);
498+
}
499+
}
500+
};
412501

413502
self.session_state
414503
.add_tokens(response.usage.input_tokens, response.usage.output_tokens);

src/tools/bashexec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::process::Command;
66
use std::sync::{Arc, Mutex};
77

88
const MAX_OUTPUT_SIZE: usize = 10 * 1024 * 1024; // 10MB limit
9-
const MAX_TOOL_OUTPUT_TOKENS: usize = 8_000; // ~28KB, prevents excessive context usage
9+
const MAX_TOOL_OUTPUT_TOKENS: usize = 16_000; // ~56KB, prevents excessive context usage
1010

1111
/// Truncate bash output if it exceeds token limit for context efficiency
1212
fn truncate_for_context(content: &str, max_tokens: usize) -> String {

src/tools/filesystem.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fs;
44
use std::path::{Path, PathBuf};
55

66
const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; // 50MB limit
7-
const MAX_TOOL_OUTPUT_TOKENS: usize = 8_000; // ~28KB, prevents excessive context usage
7+
const MAX_TOOL_OUTPUT_TOKENS: usize = 16_000; // ~56KB, prevents excessive context usage
88

99
/// Truncate file content if it exceeds token limit for context efficiency
1010
fn truncate_for_context(content: &str, max_tokens: usize) -> String {

0 commit comments

Comments
 (0)