diff --git a/crates/brightstaff/src/handlers/chat_completions.rs b/crates/brightstaff/src/handlers/chat_completions.rs index bd5cab79..37da961f 100644 --- a/crates/brightstaff/src/handlers/chat_completions.rs +++ b/crates/brightstaff/src/handlers/chat_completions.rs @@ -32,6 +32,8 @@ pub async fn chat_completions( let chat_request_bytes = request.collect().await?.to_bytes(); + debug!("Received request body (raw utf8): {}", String::from_utf8_lossy(&chat_request_bytes)); + let chat_request_parsed = serde_json::from_slice::(&chat_request_bytes) .inspect_err(|err| { warn!( diff --git a/crates/hermesllm/src/providers/openai/types.rs b/crates/hermesllm/src/providers/openai/types.rs index d1c4430c..7dea64df 100644 --- a/crates/hermesllm/src/providers/openai/types.rs +++ b/crates/hermesllm/src/providers/openai/types.rs @@ -35,9 +35,16 @@ pub enum MultiPartContentType { ImageUrl, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ImageUrl { + pub url: String, +} + +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct MultiPartContent { pub text: Option, + pub image_url: Option, #[serde(rename = "type")] pub content_type: MultiPartContentType, } @@ -307,10 +314,12 @@ mod tests { MultiPartContent { text: Some("This is a text part.".to_string()), content_type: MultiPartContentType::Text, + image_url: None, }, MultiPartContent { text: Some("https://example.com/image.png".to_string()), content_type: MultiPartContentType::ImageUrl, + image_url: None, }, ]); assert_eq!(multi_part_content.to_string(), "This is a text part."); @@ -364,6 +373,61 @@ mod tests { } } + #[test] + fn test_chat_completions_request_image_content() { + const CHAT_COMPLETIONS_REQUEST: &str = r#" + { + "stream": true, + "model": "openai/gpt-4o", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "describe this photo pls" + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/...==" + } + } + ] + } + ] + }"#; + + let chat_completions_request: ChatCompletionsRequest = + serde_json::from_str(CHAT_COMPLETIONS_REQUEST).unwrap(); + assert_eq!(chat_completions_request.model, "openai/gpt-4o"); + if let Some(ContentType::MultiPart(multi_part_content)) = + chat_completions_request.messages[0].content.as_ref() + { + assert_eq!(multi_part_content.len(), 2); + assert_eq!( + multi_part_content[0].content_type, + MultiPartContentType::Text + ); + assert_eq!( + multi_part_content[0].text, + Some("describe this photo pls".to_string()) + ); + assert_eq!( + multi_part_content[1].content_type, + MultiPartContentType::ImageUrl + ); + assert_eq!( + multi_part_content[1].image_url, + Some(ImageUrl { + url: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/...==".to_string(), + }) + ); + } else { + panic!("Expected MultiPartContent"); + } + } + #[test] fn test_sse_streaming() { let json_data = r#"data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} diff --git a/demos/use_cases/llm_routing/arch_config.yaml b/demos/use_cases/llm_routing/arch_config.yaml index cb3a42e6..addaae66 100644 --- a/demos/use_cases/llm_routing/arch_config.yaml +++ b/demos/use_cases/llm_routing/arch_config.yaml @@ -12,6 +12,9 @@ llm_providers: - access_key: $OPENAI_API_KEY model: openai/gpt-4o-mini + - access_key: $OPENAI_API_KEY + model: openai/gpt-4.1 + - access_key: $OPENAI_API_KEY model: openai/gpt-4o default: true