From 78f90234da844701513be980db47f14c59b94a03 Mon Sep 17 00:00:00 2001 From: Salman Paracha Date: Fri, 2 Jan 2026 12:07:53 -0800 Subject: [PATCH] fixing the messages construction and using the trait for signals --- crates/brightstaff/src/handlers/llm.rs | 17 +- crates/hermesllm/src/apis/openai_responses.rs | 82 +----- .../src/transforms/request/from_openai.rs | 259 ++++++++++-------- 3 files changed, 160 insertions(+), 198 deletions(-) diff --git a/crates/brightstaff/src/handlers/llm.rs b/crates/brightstaff/src/handlers/llm.rs index 3361f68b..b3320cc3 100644 --- a/crates/brightstaff/src/handlers/llm.rs +++ b/crates/brightstaff/src/handlers/llm.rs @@ -112,7 +112,7 @@ pub async fn llm_chat( .map(|msg| truncate_message(&msg, 50)); // Extract messages for signal analysis (clone before moving client_request) - let messages_for_signals = extract_messages_for_signals(&client_request); + let messages_for_signals = client_request.get_messages(); client_request.set_model(resolved_model.clone()); if client_request.remove_metadata_key("archgw_preference_config") { @@ -298,8 +298,8 @@ pub async fn llm_chat( ); // Add messages for signal analysis if available - if let Some(messages) = messages_for_signals { - base_processor = base_processor.with_messages(messages); + if !messages_for_signals.is_empty() { + base_processor = base_processor.with_messages(messages_for_signals); } // === v1/responses state management: Wrap with ResponsesStateProcessor === @@ -499,14 +499,3 @@ async fn get_provider_info( (hermesllm::ProviderId::OpenAI, None) } } - -/// Extract messages from ProviderRequestType for signal analysis -/// Returns None for non-ChatCompletions requests -fn extract_messages_for_signals( - request: &ProviderRequestType, -) -> Option> { - match request { - ProviderRequestType::ChatCompletionsRequest(chat_req) => Some(chat_req.messages.clone()), - _ => None, - } -} diff --git a/crates/hermesllm/src/apis/openai_responses.rs b/crates/hermesllm/src/apis/openai_responses.rs index e49173dc..720e24d3 100644 --- a/crates/hermesllm/src/apis/openai_responses.rs +++ b/crates/hermesllm/src/apis/openai_responses.rs @@ -1127,82 +1127,16 @@ impl ProviderRequest for ResponsesAPIRequest { } fn get_messages(&self) -> Vec { - use crate::apis::openai::{Message, MessageContent, Role}; + use crate::transforms::request::from_openai::ResponsesInputConverter; - let mut openai_messages = Vec::new(); + // Use the shared converter to get the full conversion with image support + let converter = ResponsesInputConverter { + input: self.input.clone(), + instructions: self.instructions.clone(), + }; - // Add instructions as system message if present - if let Some(instructions) = &self.instructions { - openai_messages.push(Message { - role: Role::System, - content: MessageContent::Text(instructions.clone()), - name: None, - tool_calls: None, - tool_call_id: None, - }); - } - - // Convert input to messages - match &self.input { - InputParam::Text(text) => { - openai_messages.push(Message { - role: Role::User, - content: MessageContent::Text(text.clone()), - name: None, - tool_calls: None, - tool_call_id: None, - }); - } - InputParam::Items(items) => { - for item in items { - match item { - InputItem::Message(msg) => { - // Convert message role - let role = match msg.role { - MessageRole::User => Role::User, - MessageRole::Assistant => Role::Assistant, - MessageRole::System => Role::System, - MessageRole::Developer => Role::System, // Map developer to system - }; - - // Extract text from message content - let content = match &msg.content { - crate::apis::openai_responses::MessageContent::Text(text) => { - text.clone() - } - crate::apis::openai_responses::MessageContent::Items(items) => { - items - .iter() - .filter_map(|c| { - if let InputContent::InputText { text } = c { - Some(text.clone()) - } else { - None - } - }) - .collect::>() - .join("\n") - } - }; - - openai_messages.push(Message { - role, - content: MessageContent::Text(content), - name: None, - tool_calls: None, - tool_call_id: None, - }); - } - // Skip other input item types for now - InputItem::ItemReference { .. } | InputItem::FunctionCallOutput { .. } => { - // These are not yet supported in agent framework - } - } - } - } - } - - openai_messages + // Convert and return, falling back to empty vec on error + converter.try_into().unwrap_or_else(|_| Vec::new()) } fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { diff --git a/crates/hermesllm/src/transforms/request/from_openai.rs b/crates/hermesllm/src/transforms/request/from_openai.rs index 2a133041..e39cfed3 100644 --- a/crates/hermesllm/src/transforms/request/from_openai.rs +++ b/crates/hermesllm/src/transforms/request/from_openai.rs @@ -24,6 +24,150 @@ use crate::transforms::*; type AnthropicMessagesRequest = MessagesRequest; +// ============================================================================ +// RESPONSES API INPUT CONVERSION +// ============================================================================ + +/// Helper struct for converting ResponsesAPI input to OpenAI messages +pub struct ResponsesInputConverter { + pub input: InputParam, + pub instructions: Option, +} + +impl TryFrom for Vec { + type Error = TransformError; + + fn try_from(converter: ResponsesInputConverter) -> Result { + // Convert input to messages + match converter.input { + InputParam::Text(text) => { + // Simple text input becomes a user message + let mut messages = Vec::new(); + + // Add instructions as system message if present + if let Some(instructions) = converter.instructions { + messages.push(Message { + role: Role::System, + content: MessageContent::Text(instructions), + name: None, + tool_call_id: None, + tool_calls: None, + }); + } + + // Add the user message + messages.push(Message { + role: Role::User, + content: MessageContent::Text(text), + name: None, + tool_call_id: None, + tool_calls: None, + }); + + Ok(messages) + } + InputParam::Items(items) => { + // Convert input items to messages + let mut converted_messages = Vec::new(); + + // Add instructions as system message if present + if let Some(instructions) = converter.instructions { + converted_messages.push(Message { + role: Role::System, + content: MessageContent::Text(instructions), + name: None, + tool_call_id: None, + tool_calls: None, + }); + } + + // Convert each input item + for item in items { + if let InputItem::Message(input_msg) = item { + let role = match input_msg.role { + MessageRole::User => Role::User, + MessageRole::Assistant => Role::Assistant, + MessageRole::System => Role::System, + MessageRole::Developer => Role::System, // Map developer to system + }; + + // Convert content based on MessageContent type + let content = match &input_msg.content { + crate::apis::openai_responses::MessageContent::Text(text) => { + // Simple text content + MessageContent::Text(text.clone()) + } + crate::apis::openai_responses::MessageContent::Items(content_items) => { + // Check if it's a single text item (can use simple text format) + if content_items.len() == 1 { + if let InputContent::InputText { text } = &content_items[0] { + MessageContent::Text(text.clone()) + } else { + // Single non-text item - use parts format + MessageContent::Parts( + content_items.iter() + .filter_map(|c| match c { + InputContent::InputText { text } => { + Some(crate::apis::openai::ContentPart::Text { text: text.clone() }) + } + InputContent::InputImage { image_url, .. } => { + Some(crate::apis::openai::ContentPart::ImageUrl { + image_url: crate::apis::openai::ImageUrl { + url: image_url.clone(), + detail: None, + } + }) + } + InputContent::InputFile { .. } => None, // Skip files for now + InputContent::InputAudio { .. } => None, // Skip audio for now + }) + .collect() + ) + } + } else { + // Multiple content items - convert to parts + MessageContent::Parts( + content_items + .iter() + .filter_map(|c| match c { + InputContent::InputText { text } => { + Some(crate::apis::openai::ContentPart::Text { + text: text.clone(), + }) + } + InputContent::InputImage { image_url, .. } => Some( + crate::apis::openai::ContentPart::ImageUrl { + image_url: crate::apis::openai::ImageUrl { + url: image_url.clone(), + detail: None, + }, + }, + ), + InputContent::InputFile { .. } => None, // Skip files for now + InputContent::InputAudio { .. } => None, // Skip audio for now + }) + .collect(), + ) + } + } + }; + + converted_messages.push(Message { + role, + content, + name: None, + tool_call_id: None, + tool_calls: None, + }); + } + } + + Ok(converted_messages) + } + } + } +} + // ============================================================================ // MAIN REQUEST TRANSFORMATIONS // ============================================================================ @@ -253,117 +397,12 @@ impl TryFrom for ChatCompletionsRequest { type Error = TransformError; fn try_from(req: ResponsesAPIRequest) -> Result { - // Convert input to messages - let messages = match req.input { - InputParam::Text(text) => { - // Simple text input becomes a user message - vec![Message { - role: Role::User, - content: MessageContent::Text(text), - name: None, - tool_call_id: None, - tool_calls: None, - }] - } - InputParam::Items(items) => { - // Convert input items to messages - let mut converted_messages = Vec::new(); - - // Add instructions as system message if present - if let Some(instructions) = &req.instructions { - converted_messages.push(Message { - role: Role::System, - content: MessageContent::Text(instructions.clone()), - name: None, - tool_call_id: None, - tool_calls: None, - }); - } - - // Convert each input item - for item in items { - if let InputItem::Message(input_msg) = item { - let role = match input_msg.role { - MessageRole::User => Role::User, - MessageRole::Assistant => Role::Assistant, - MessageRole::System => Role::System, - MessageRole::Developer => Role::System, // Map developer to system - }; - - // Convert content based on MessageContent type - let content = match &input_msg.content { - crate::apis::openai_responses::MessageContent::Text(text) => { - // Simple text content - MessageContent::Text(text.clone()) - } - crate::apis::openai_responses::MessageContent::Items(content_items) => { - // Check if it's a single text item (can use simple text format) - if content_items.len() == 1 { - if let InputContent::InputText { text } = &content_items[0] { - MessageContent::Text(text.clone()) - } else { - // Single non-text item - use parts format - MessageContent::Parts( - content_items.iter() - .filter_map(|c| match c { - InputContent::InputText { text } => { - Some(crate::apis::openai::ContentPart::Text { text: text.clone() }) - } - InputContent::InputImage { image_url, .. } => { - Some(crate::apis::openai::ContentPart::ImageUrl { - image_url: crate::apis::openai::ImageUrl { - url: image_url.clone(), - detail: None, - } - }) - } - InputContent::InputFile { .. } => None, // Skip files for now - InputContent::InputAudio { .. } => None, // Skip audio for now - }) - .collect() - ) - } - } else { - // Multiple content items - convert to parts - MessageContent::Parts( - content_items - .iter() - .filter_map(|c| match c { - InputContent::InputText { text } => { - Some(crate::apis::openai::ContentPart::Text { - text: text.clone(), - }) - } - InputContent::InputImage { image_url, .. } => Some( - crate::apis::openai::ContentPart::ImageUrl { - image_url: crate::apis::openai::ImageUrl { - url: image_url.clone(), - detail: None, - }, - }, - ), - InputContent::InputFile { .. } => None, // Skip files for now - InputContent::InputAudio { .. } => None, // Skip audio for now - }) - .collect(), - ) - } - } - }; - - converted_messages.push(Message { - role, - content, - name: None, - tool_call_id: None, - tool_calls: None, - }); - } - } - - converted_messages - } + // Convert input to messages using the shared converter + let converter = ResponsesInputConverter { + input: req.input, + instructions: req.instructions.clone(), }; + let messages: Vec = converter.try_into()?; // Build the ChatCompletionsRequest Ok(ChatCompletionsRequest {