language_models: Add images support to LMStudio provider (#32741)

Tested with gemma3:4b
LMStudio: beta version 0.3.17

Release Notes:

- Add images support to LMStudio provider
This commit is contained in:
Umesh Yadav 2025-06-17 15:44:44 +05:30 committed by GitHub
parent 6ad9a66cf9
commit 4b88090cca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 190 additions and 34 deletions

View file

@ -48,6 +48,7 @@ pub struct Model {
pub display_name: Option<String>,
pub max_tokens: usize,
pub supports_tool_calls: bool,
pub supports_images: bool,
}
impl Model {
@ -56,12 +57,14 @@ impl Model {
display_name: Option<&str>,
max_tokens: Option<usize>,
supports_tool_calls: bool,
supports_images: bool,
) -> Self {
Self {
name: name.to_owned(),
display_name: display_name.map(|s| s.to_owned()),
max_tokens: max_tokens.unwrap_or(2048),
supports_tool_calls,
supports_images,
}
}
@ -110,22 +113,78 @@ pub struct FunctionDefinition {
pub enum ChatMessage {
Assistant {
#[serde(default)]
content: Option<String>,
content: Option<MessageContent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
},
User {
content: String,
content: MessageContent,
},
System {
content: String,
content: MessageContent,
},
Tool {
content: String,
content: MessageContent,
tool_call_id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
Plain(String),
Multipart(Vec<MessagePart>),
}
impl MessageContent {
pub fn empty() -> Self {
MessageContent::Multipart(vec![])
}
pub fn push_part(&mut self, part: MessagePart) {
match self {
MessageContent::Plain(text) => {
*self =
MessageContent::Multipart(vec![MessagePart::Text { text: text.clone() }, part]);
}
MessageContent::Multipart(parts) if parts.is_empty() => match part {
MessagePart::Text { text } => *self = MessageContent::Plain(text),
MessagePart::Image { .. } => *self = MessageContent::Multipart(vec![part]),
},
MessageContent::Multipart(parts) => parts.push(part),
}
}
}
impl From<Vec<MessagePart>> for MessageContent {
fn from(mut parts: Vec<MessagePart>) -> Self {
if let [MessagePart::Text { text }] = parts.as_mut_slice() {
MessageContent::Plain(std::mem::take(text))
} else {
MessageContent::Multipart(parts)
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessagePart {
Text {
text: String,
},
#[serde(rename = "image_url")]
Image {
image_url: ImageUrl,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
@ -210,6 +269,10 @@ impl Capabilities {
pub fn supports_tool_calls(&self) -> bool {
self.0.iter().any(|cap| cap == "tool_use")
}
pub fn supports_images(&self) -> bool {
self.0.iter().any(|cap| cap == "vision")
}
}
#[derive(Serialize, Deserialize, Debug)]
@ -393,3 +456,38 @@ pub async fn get_models(
serde_json::from_str(&body).context("Unable to parse LM Studio models response")?;
Ok(response.data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_message_part_serialization() {
let image_part = MessagePart::Image {
image_url: ImageUrl {
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==".to_string(),
detail: None,
},
};
let json = serde_json::to_string(&image_part).unwrap();
println!("Serialized image part: {}", json);
// Verify the structure matches what LM Studio expects
let expected_structure = r#"{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="}}"#;
assert_eq!(json, expected_structure);
}
#[test]
fn test_text_message_part_serialization() {
let text_part = MessagePart::Text {
text: "Hello, world!".to_string(),
};
let json = serde_json::to_string(&text_part).unwrap();
println!("Serialized text part: {}", json);
let expected_structure = r#"{"type":"text","text":"Hello, world!"}"#;
assert_eq!(json, expected_structure);
}
}