From 829c2c115d739ccbc6d032383ad4dfd20f4e9a19 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Fri, 23 Oct 2026 06:02:00 +0530 Subject: [PATCH 1/2] fix(openai): folded system messages keep their JSON blocks `single_system_message` and `merge_system_into_user` gathered system content with a text-only helper that dropped JSON blocks and joined blocks with a newline. The pass-through path (`translate_text_content`) serializes JSON blocks and joins blocks with nothing between them, so a prompt sent to a hoisting or merging endpoint differed from the same prompt sent as-is, and lost any structured block. Render folded system content the same way. --- .../src/providers/openai/mod_tests.rs | 25 +++++++++++++++++++ .../src/providers/openai/transport.rs | 13 ++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/crates/tinyinference-llm/src/providers/openai/mod_tests.rs b/crates/tinyinference-llm/src/providers/openai/mod_tests.rs index 76fab26e..81d25f3a 100644 --- a/crates/tinyinference-llm/src/providers/openai/mod_tests.rs +++ b/crates/tinyinference-llm/src/providers/openai/mod_tests.rs @@ -2003,6 +2003,31 @@ fn merge_system_preserves_user_image_blocks() { } } +#[test] +fn folded_system_messages_keep_their_json_blocks_as_sent_unfolded() { + // A system message's JSON block is serialized on the pass-through path; + // hoisting or merging it into the user turn must not drop it, and its + // blocks join exactly as the pass-through path joins them. + use crate::message::SystemMessage; + let structured = Message::System(SystemMessage { + content: vec![ + ContentBlock::Text("Context: ".to_string()), + ContentBlock::Json(serde_json::json!({"tenant": "acme"})), + ], + ..Default::default() + }); + let sent_as_is = r#"Context: {"tenant":"acme"}"#; + + let hoisted = single_system_message(&[ + Message::user("hi"), + structured.clone(), + ]); + assert_eq!(hoisted[0].text(), sent_as_is); + + let merged = merge_system_into_user(&[structured, Message::user("hi")]); + assert_eq!(merged[0].text(), format!("{sent_as_is}\n\nhi")); +} + #[test] fn merge_system_is_noop_without_system_messages() { let input = vec![Message::user("just user")]; diff --git a/crates/tinyinference-llm/src/providers/openai/transport.rs b/crates/tinyinference-llm/src/providers/openai/transport.rs index e7fa8bfc..b17b4ebf 100644 --- a/crates/tinyinference-llm/src/providers/openai/transport.rs +++ b/crates/tinyinference-llm/src/providers/openai/transport.rs @@ -186,17 +186,20 @@ pub(super) fn auth_headers(auth: &AuthStyle, api_key: &str) -> Vec<(String, Stri } } -/// Concatenates the text of a message's content blocks (non-text blocks — images, -/// json, thinking — are ignored). Used to gather system-prompt text for merging. +/// A system message's content as the pass-through path sends it +/// (`translate_text_content`): text verbatim, JSON serialized, reasoning +/// dropped, blocks joined with nothing between them. Folding with anything +/// else would make a hoisted or merged prompt differ from the same prompt sent +/// as-is, and would silently drop JSON blocks. fn content_text(content: &[ContentBlock]) -> String { content .iter() .filter_map(|block| match block { - ContentBlock::Text(text) => Some(text.as_str()), + ContentBlock::Text(text) => Some(text.clone()), + ContentBlock::Json(value) => Some(value.to_string()), _ => None, }) - .collect::>() - .join("\n") + .collect() } /// Folds all `system` messages into the first `user` message and drops the From be71b6eb8107d01779e17b6af7a35c0cb0bdb9e8 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Fri, 23 Oct 2026 06:03:20 +0530 Subject: [PATCH 2/2] style: rustfmt the folding test --- crates/tinyinference-llm/src/providers/openai/mod_tests.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/tinyinference-llm/src/providers/openai/mod_tests.rs b/crates/tinyinference-llm/src/providers/openai/mod_tests.rs index 81d25f3a..8f90e7a1 100644 --- a/crates/tinyinference-llm/src/providers/openai/mod_tests.rs +++ b/crates/tinyinference-llm/src/providers/openai/mod_tests.rs @@ -2018,10 +2018,7 @@ fn folded_system_messages_keep_their_json_blocks_as_sent_unfolded() { }); let sent_as_is = r#"Context: {"tenant":"acme"}"#; - let hoisted = single_system_message(&[ - Message::user("hi"), - structured.clone(), - ]); + let hoisted = single_system_message(&[Message::user("hi"), structured.clone()]); assert_eq!(hoisted[0].text(), sent_as_is); let merged = merge_system_into_user(&[structured, Message::user("hi")]);