diff --git a/crates/tinyagents-harness/src/providers/claude_code/input_builder_tests.rs b/crates/tinyagents-harness/src/providers/claude_code/input_builder_tests.rs index 0f7669a2..56ce0f10 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/input_builder_tests.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/input_builder_tests.rs @@ -161,6 +161,23 @@ fn image_blocks_preserve_text_order() { assert_eq!(content[4]["text"], " after"); } +#[test] +fn literal_native_image_marker_stays_text() { + let s = String::from_utf8(build_stdin( + &[ChatMessage::user( + "literal [OH_IMAGE:data:image/png;base64,QUJD] then [OH_IMAGE_LITERAL:data:image/png;base64,REVG]", + )], + true, + )) + .unwrap(); + let row: Value = serde_json::from_str(s.lines().next().unwrap()).unwrap(); + let content = row["message"]["content"].as_array().unwrap(); + assert_eq!(content[0]["text"], "literal "); + assert_eq!(content[1]["type"], "image"); + assert_eq!(content[2]["text"], " then "); + assert_eq!(content[3]["text"], "[OH_IMAGE:data:image/png;base64,REVG]"); +} + #[test] fn literal_file_marker_is_not_read() { let s = String::from_utf8(build_stdin( diff --git a/crates/tinyagents-harness/src/providers/claude_code/mod.rs b/crates/tinyagents-harness/src/providers/claude_code/mod.rs index 615a6982..639b3f0f 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/mod.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/mod.rs @@ -369,14 +369,19 @@ fn response_format_instruction(format: Option<&ResponseFormat>) -> Option]` marker `input_builder` later rehydrates, +/// blocks pass through (with internal image-looking text escaped), +/// JSON/extension blocks are stringified, an image becomes an +/// `[OH_IMAGE:]` marker `input_builder` later rehydrates, /// and redacted-thinking blocks are dropped (nothing to show). fn render_content(content: &[ContentBlock]) -> String { content .iter() .filter_map(|block| match block { - ContentBlock::Text(text) => Some(text.clone()), + // `[OH_IMAGE:…]` is an internal transport marker. User-authored + // text that happens to contain that spelling must remain prose, + // rather than becoming an unintended native attachment when the + // input builder rehydrates image markers below. + ContentBlock::Text(text) => Some(text.replace("[OH_IMAGE:", "[OH_IMAGE_LITERAL:")), ContentBlock::Image(image) => Some(format!("[OH_IMAGE:{}]", image.url)), ContentBlock::Json(value) | ContentBlock::ProviderExtension(value) => { Some(value.to_string())