Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions crates/lingua/src/import_parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::serde_json::{self, Value};
use crate::universal::convert::TryFromLLM;
use crate::universal::Message;
use serde::de::DeserializeOwned;

pub(crate) type MessageParser = fn(&Value) -> Option<Vec<Message>>;

pub(crate) fn try_parsers_in_order(
data: &Value,
parsers: &[MessageParser],
) -> Option<Vec<Message>> {
for parser in parsers {
if let Some(messages) = parser(data) {
if !messages.is_empty() {
return Some(messages);
}
}
}
None
}

pub(crate) fn non_empty_messages(messages: Vec<Message>) -> Option<Vec<Message>> {
if messages.is_empty() {
None
} else {
Some(messages)
}
}

pub(crate) fn try_parse<T>(data: &Value) -> Option<T>
where
T: DeserializeOwned,
{
serde_json::from_value::<T>(data.clone()).ok()
}

pub(crate) fn try_convert_non_empty<T>(value: T) -> Option<Vec<Message>>
where
Vec<Message>: TryFromLLM<T>,
{
let messages = <Vec<Message> as TryFromLLM<T>>::try_from(value).ok()?;
non_empty_messages(messages)
}

pub(crate) fn try_parse_and_convert<T>(data: &Value) -> Option<Vec<Message>>
where
T: DeserializeOwned,
Vec<Message>: TryFromLLM<T>,
{
let value = try_parse::<T>(data)?;
try_convert_non_empty(value)
}

pub(crate) fn try_parse_vec_or_single<T>(data: &Value) -> Option<Vec<T>>
where
T: DeserializeOwned,
{
match data {
Value::Array(_) => try_parse::<Vec<T>>(data),
Value::Object(_) => try_parse::<T>(data).map(|item| vec![item]),
_ => None,
}
}
1 change: 1 addition & 0 deletions crates/lingua/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use bytes::Bytes;
pub mod capabilities;
pub mod error;
mod extraction;
mod import_parse;
pub mod processing;
pub mod providers;
pub mod universal;
Expand Down
60 changes: 47 additions & 13 deletions crates/lingua/src/processing/import.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
use crate::import_parse::{try_parsers_in_order, MessageParser};
#[cfg(feature = "anthropic")]
use crate::providers::anthropic::convert::try_parse_anthropic_for_import;
#[cfg(feature = "anthropic")]
use crate::providers::anthropic::generated as anthropic;
#[cfg(feature = "bedrock")]
use crate::providers::bedrock::convert::try_parse_bedrock_for_import;
#[cfg(feature = "google")]
use crate::providers::google::convert::try_parse_google_for_import;
#[cfg(feature = "openai")]
use crate::providers::openai::convert::{
try_parse_responses_items_for_import, ChatCompletionRequestMessageExt,
try_parse_openai_for_import, ChatCompletionRequestMessageExt,
};
use crate::serde_json;
use crate::serde_json::Value;
Expand All @@ -25,7 +34,7 @@ pub struct Span {

/// Try to convert a value to lingua messages by attempting multiple format conversions
fn try_converting_to_messages(data: &Value) -> Vec<Message> {
if let Some(messages) = try_parse_responses_items_for_import(data) {
if let Some(messages) = try_parse_provider_messages_for_import(data) {
return messages;
}

Expand Down Expand Up @@ -85,24 +94,29 @@ fn try_converting_to_messages(data: &Value) -> Vec<Message> {

// Try Chat Completions format (most common)
// Use extended type to capture reasoning field from vLLM/OpenRouter convention
if let Ok(provider_messages) =
serde_json::from_value::<Vec<ChatCompletionRequestMessageExt>>(data_to_parse.clone())
#[cfg(feature = "openai")]
{
if let Ok(messages) =
<Vec<Message> as TryFromLLM<Vec<ChatCompletionRequestMessageExt>>>::try_from(
provider_messages,
)
if let Ok(provider_messages) =
serde_json::from_value::<Vec<ChatCompletionRequestMessageExt>>(data_to_parse.clone())
{
if !messages.is_empty() {
return messages;
if let Ok(messages) = <Vec<Message> as TryFromLLM<
Vec<ChatCompletionRequestMessageExt>,
>>::try_from(provider_messages)
{
if !messages.is_empty() {
return messages;
}
}
}
}

// Try Anthropic format (including role-based system/developer messages).
if let Some(anthropic_messages) = try_anthropic_or_system_messages(data_to_parse) {
if !anthropic_messages.is_empty() {
return anthropic_messages;
#[cfg(feature = "anthropic")]
{
if let Some(anthropic_messages) = try_anthropic_or_system_messages(data_to_parse) {
if !anthropic_messages.is_empty() {
return anthropic_messages;
}
}
}

Expand All @@ -124,26 +138,45 @@ fn try_converting_to_messages(data: &Value) -> Vec<Message> {
Vec::new()
}

fn try_parse_provider_messages_for_import(data: &Value) -> Option<Vec<Message>> {
let provider_parsers: Vec<MessageParser> = vec![
#[cfg(feature = "openai")]
try_parse_openai_for_import,
#[cfg(feature = "anthropic")]
try_parse_anthropic_for_import,
#[cfg(feature = "google")]
try_parse_google_for_import,
#[cfg(feature = "bedrock")]
try_parse_bedrock_for_import,
];

try_parsers_in_order(data, &provider_parsers)
}

#[cfg(feature = "anthropic")]
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum AnthropicOrSystemMessage {
Anthropic(anthropic::InputMessage),
SystemOrDeveloper(SystemOrDeveloperMessage),
}

#[cfg(feature = "anthropic")]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SystemOrDeveloperMessage {
role: SystemOrDeveloperRole,
content: Value,
}

#[cfg(feature = "anthropic")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum SystemOrDeveloperRole {
System,
Developer,
}

#[cfg(feature = "anthropic")]
fn try_parse_anthropic_or_system_message(item: AnthropicOrSystemMessage) -> Option<Message> {
match item {
AnthropicOrSystemMessage::Anthropic(provider_message) => {
Expand All @@ -156,6 +189,7 @@ fn try_parse_anthropic_or_system_message(item: AnthropicOrSystemMessage) -> Opti
}
}

#[cfg(feature = "anthropic")]
fn try_anthropic_or_system_messages(data: &Value) -> Option<Vec<Message>> {
let items: Vec<AnthropicOrSystemMessage> = serde_json::from_value(data.clone()).ok()?;
if items.is_empty() {
Expand Down
62 changes: 62 additions & 0 deletions crates/lingua/src/providers/anthropic/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ use std::convert::TryFrom;

use crate::capabilities::ProviderFormat;
use crate::error::ConvertError;
use crate::import_parse::{
try_convert_non_empty, try_parse, try_parse_and_convert, try_parse_vec_or_single,
try_parsers_in_order, MessageParser,
};
use crate::providers::anthropic::generated;
use crate::providers::anthropic::generated::{
CustomTool, JsonOutputFormat, JsonOutputFormatType, Tool, ToolChoice, ToolChoiceType,
Expand Down Expand Up @@ -867,6 +871,64 @@ impl TryFromLLM<Message> for generated::InputMessage {
}
}

pub(crate) fn try_parse_content_blocks_for_import(
data: &serde_json::Value,
) -> Option<Vec<Message>> {
let blocks = try_parse_vec_or_single::<generated::ContentBlock>(data)?;
try_convert_non_empty(blocks)
}

fn try_messages_from_anthropic_request(
request: generated::CreateMessageParams,
) -> Option<Vec<Message>> {
let mut messages = Vec::new();

if let Some(system) = request.system {
messages.push(Message::System {
content: system_to_user_content(system),
});
}

let mut request_messages =
<Vec<Message> as TryFromLLM<Vec<generated::InputMessage>>>::try_from(request.messages)
.ok()?;
messages.append(&mut request_messages);

if messages.is_empty() {
None
} else {
Some(messages)
}
}

fn try_messages_from_anthropic_response(response: generated::Message) -> Option<Vec<Message>> {
try_convert_non_empty(response.content)
}

fn try_parse_input_messages_for_import(data: &serde_json::Value) -> Option<Vec<Message>> {
try_parse_and_convert::<Vec<generated::InputMessage>>(data)
}

fn try_parse_anthropic_request_for_import(data: &serde_json::Value) -> Option<Vec<Message>> {
let request = try_parse::<generated::CreateMessageParams>(data)?;
try_messages_from_anthropic_request(request)
}

fn try_parse_anthropic_response_for_import(data: &serde_json::Value) -> Option<Vec<Message>> {
let response = try_parse::<generated::Message>(data)?;
try_messages_from_anthropic_response(response)
}

pub(crate) fn try_parse_anthropic_for_import(data: &serde_json::Value) -> Option<Vec<Message>> {
const PARSERS: &[MessageParser] = &[
try_parse_content_blocks_for_import,
try_parse_input_messages_for_import,
try_parse_anthropic_request_for_import,
try_parse_anthropic_response_for_import,
];
try_parsers_in_order(data, PARSERS)
}

// Convert from Anthropic response ContentBlock to Universal Message
impl TryFromLLM<Vec<generated::ContentBlock>> for Vec<Message> {
type Error = ConvertError;
Expand Down
66 changes: 65 additions & 1 deletion crates/lingua/src/providers/bedrock/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ AWS Bedrock's Converse API format and Lingua's universal message format.
*/

use crate::error::ConvertError;
use crate::import_parse::{
try_convert_non_empty, try_parse, try_parse_vec_or_single, try_parsers_in_order, MessageParser,
};
use crate::providers::bedrock::request::{
BedrockContentBlock, BedrockConversationRole, BedrockImageBlock, BedrockImageFormat,
BedrockImageSource, BedrockMessage, BedrockToolResultBlock, BedrockToolResultContent,
BedrockToolUseBlock, ConverseRequest,
};
use crate::providers::bedrock::response::{BedrockOutputContentBlock, BedrockOutputMessage};
use crate::providers::bedrock::response::{
BedrockOutputContentBlock, BedrockOutputMessage, ConverseResponse,
};
use crate::serde_json::{self, Value};
use crate::universal::convert::TryFromLLM;
use crate::universal::message::{
Expand Down Expand Up @@ -282,6 +287,65 @@ pub fn universal_to_bedrock(messages: &[Message]) -> Result<Value, ConvertError>
})
}

fn try_messages_from_bedrock_messages(messages: Vec<BedrockMessage>) -> Option<Vec<Message>> {
try_convert_non_empty(messages)
}

fn try_message_from_bedrock_output_message(message: BedrockOutputMessage) -> Option<Vec<Message>> {
if message.role != "assistant" {
return None;
}

let message = <Message as TryFromLLM<BedrockOutputMessage>>::try_from(message).ok()?;
Some(vec![message])
}

fn try_messages_from_bedrock_output_messages(
output_messages: Vec<BedrockOutputMessage>,
) -> Option<Vec<Message>> {
if output_messages
.iter()
.any(|message| message.role != "assistant")
{
return None;
}

try_convert_non_empty(output_messages)
}

fn try_parse_bedrock_message_for_import(data: &Value) -> Option<Vec<Message>> {
let messages = try_parse_vec_or_single::<BedrockMessage>(data)?;
try_messages_from_bedrock_messages(messages)
}

fn try_parse_bedrock_request_for_import(data: &Value) -> Option<Vec<Message>> {
let request = try_parse::<ConverseRequest>(data)?;
try_messages_from_bedrock_messages(request.messages)
}

fn try_parse_bedrock_output_message_for_import(data: &Value) -> Option<Vec<Message>> {
let output_messages = try_parse_vec_or_single::<BedrockOutputMessage>(data)?;
if output_messages.len() == 1 {
return try_message_from_bedrock_output_message(output_messages.into_iter().next()?);
}
try_messages_from_bedrock_output_messages(output_messages)
}

fn try_parse_bedrock_response_for_import(data: &Value) -> Option<Vec<Message>> {
let response = try_parse::<ConverseResponse>(data)?;
try_message_from_bedrock_output_message(response.output.message)
}

pub(crate) fn try_parse_bedrock_for_import(data: &Value) -> Option<Vec<Message>> {
const PARSERS: &[MessageParser] = &[
try_parse_bedrock_message_for_import,
try_parse_bedrock_request_for_import,
try_parse_bedrock_output_message_for_import,
try_parse_bedrock_response_for_import,
];
try_parsers_in_order(data, PARSERS)
}

// ============================================================================
// BedrockOutputMessage (Response) -> Universal Message
// ============================================================================
Expand Down
Loading