use crate::config::{ResponseConfig, ResponseExtract, UsageExtract};
use crate::message_format::{
ContentBlockConfig, ContentBlockType, MessageFormatBuilder, MessageFormatConfig,
};
use crate::streaming::sse::extractors::{
EndCondition, ExtractorConfig, JsonPath, JsonPathExtractor, JsonPathSegment,
};
use std::collections::HashMap;
pub mod message_format {
use super::*;
pub fn openai() -> MessageFormatBuilder {
MessageFormatBuilder::new()
.name("openai")
.text_message_template(r#"{"role": {{json role}}, "content": {{json content}}}"#)
.multimodal_message_template(r#"{"role": {{json role}}, "content": {{json content_blocks}}}"#)
.text_block(r#"{"type": "text", "text": {{json text}}}"#)
.binary_block_with_priority(
vec!["image/jpeg".to_string()],
r#"{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,{{base64_data}}"}}"#,
10,
)
.binary_block_with_priority(
vec!["image/png".to_string()],
r#"{"type": "image_url", "image_url": {"url": "data:image/png;base64,{{base64_data}}"}}"#,
10,
)
.binary_block_with_priority(
vec!["image/gif".to_string()],
r#"{"type": "image_url", "image_url": {"url": "data:image/gif;base64,{{base64_data}}"}}"#,
10,
)
.binary_block_with_priority(
vec!["image/webp".to_string()],
r#"{"type": "image_url", "image_url": {"url": "data:image/webp;base64,{{base64_data}}"}}"#,
10,
)
.url_block_with_priority(
vec!["image/*".to_string()],
r#"{"type": "image_url", "image_url": {"url": {{json url}}, "detail": "auto"}}"#,
10,
)
.binary_block_with_priority(
vec!["audio/wav".to_string()],
r#"{"type": "input_audio", "input_audio": {"data": {{json base64_data}}, "format": "wav"}}"#,
10,
)
.binary_block_with_priority(
vec!["audio/mp3".to_string()],
r#"{"type": "input_audio", "input_audio": {"data": {{json base64_data}}, "format": "mp3"}}"#,
10,
)
.binary_block(
vec![],
r#"{"type": "file", "mime_type": {{json mime_type}}, "data": {{json base64_data}}}"#,
)
.url_block(vec![], r#"{"type": "image_url", "image_url": {"url": {{json url}}}}"#)
}
pub fn anthropic() -> MessageFormatBuilder {
MessageFormatBuilder::new()
.name("anthropic")
.text_message_template(r#"{"role": {{json role}}, "content": [{"type": "text", "text": {{json content}}}]}"#)
.multimodal_message_template(r#"{"role": {{json role}}, "content": {{json content_blocks}}}"#)
.text_block(r#"{"type": "text", "text": {{json text}}}"#)
.binary_block_with_priority(
vec!["image/jpeg".to_string()],
r#"{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["image/png".to_string()],
r#"{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["image/gif".to_string()],
r#"{"type": "image", "source": {"type": "base64", "media_type": "image/gif", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["image/webp".to_string()],
r#"{"type": "image", "source": {"type": "base64", "media_type": "image/webp", "data": {{json base64_data}}}}"#,
10,
)
.url_block_with_priority(
vec!["image/*".to_string()],
r#"{"type": "image", "source": {"type": "url", "url": {{json url}}}}"#,
10,
)
.binary_block_with_priority(
vec!["application/pdf".to_string()],
r#"{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": {{json base64_data}}}}"#,
10,
)
.binary_block(
vec!["image/*".to_string()],
r#"{"type": "image", "source": {"type": "base64", "media_type": {{json mime_type}}, "data": {{json base64_data}}}}"#,
)
}
pub fn google() -> MessageFormatBuilder {
MessageFormatBuilder::new()
.name("google")
.text_message_template(r#"{"role": {{json role}}, "parts": [{"text": {{json content}}}]}"#)
.multimodal_message_template(r#"{"role": {{json role}}, "parts": {{json content_blocks}}}"#)
.text_block(r#"{"text": {{json text}}}"#)
.binary_block_with_priority(
vec!["image/jpeg".to_string()],
r#"{"inlineData": {"mimeType": "image/jpeg", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["image/png".to_string()],
r#"{"inlineData": {"mimeType": "image/png", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["image/gif".to_string()],
r#"{"inlineData": {"mimeType": "image/gif", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["image/webp".to_string()],
r#"{"inlineData": {"mimeType": "image/webp", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["application/pdf".to_string()],
r#"{"inlineData": {"mimeType": "application/pdf", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["video/mp4".to_string()],
r#"{"inlineData": {"mimeType": "video/mp4", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["audio/wav".to_string()],
r#"{"inlineData": {"mimeType": "audio/wav", "data": {{json base64_data}}}}"#,
10,
)
.binary_block_with_priority(
vec!["audio/mp3".to_string()],
r#"{"inlineData": {"mimeType": "audio/mpeg", "data": {{json base64_data}}}}"#,
10,
)
.binary_block(
vec![],
r#"{"inlineData": {"mimeType": {{json mime_type}}, "data": {{json base64_data}}}}"#,
)
}
}
pub mod sse_parser {
use super::*;
#[derive(Debug, Clone)]
pub struct SseParserBuilder {
pub line_prefix: Option<&'static str>,
pub event_type_prefix: Option<&'static str>,
pub token_path: JsonPath,
pub end_conditions: Vec<EndCondition>,
pub end_payload: Option<&'static str>,
pub usage_paths: Option<UsagePaths>,
}
#[derive(Debug, Clone)]
pub struct UsagePaths {
pub input_tokens: JsonPath,
pub output_tokens: JsonPath,
pub total_tokens: Option<JsonPath>,
pub cache_read_tokens: Option<JsonPath>,
pub cache_write_tokens: Option<JsonPath>,
}
impl SseParserBuilder {
pub fn new() -> Self {
Self {
line_prefix: None,
event_type_prefix: None,
token_path: JsonPath(vec![]),
end_conditions: vec![],
end_payload: None,
usage_paths: None,
}
}
pub fn line_prefix(mut self, prefix: &'static str) -> Self {
self.line_prefix = Some(prefix);
self
}
pub fn event_type_prefix(mut self, prefix: &'static str) -> Self {
self.event_type_prefix = Some(prefix);
self
}
pub fn token_path(mut self, path: JsonPath) -> Self {
self.token_path = path;
self
}
pub fn end_condition(mut self, condition: EndCondition) -> Self {
self.end_conditions.push(condition);
self
}
pub fn end_payload(mut self, payload: &'static str) -> Self {
self.end_payload = Some(payload);
self
}
pub fn usage_paths(mut self, paths: UsagePaths) -> Self {
self.usage_paths = Some(paths);
self
}
pub fn build_extractor_config(&self) -> ExtractorConfig {
let json_end_condition = self
.end_conditions
.iter()
.find(|c| !matches!(c, EndCondition::PayloadEquals(_)))
.cloned()
.unwrap_or(EndCondition::None);
ExtractorConfig {
token_path: self.token_path.clone(),
json_end_condition,
end_payload: self.end_payload,
}
}
pub fn build_extractor(&self) -> JsonPathExtractor {
JsonPathExtractor::new(self.build_extractor_config())
}
}
impl Default for SseParserBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn openai() -> SseParserBuilder {
use JsonPathSegment::*;
SseParserBuilder::new()
.line_prefix("data: ")
.token_path(JsonPath(vec![
Key("choices"),
Index(0),
Key("delta"),
Key("content"),
]))
.end_payload("[DONE]")
.end_condition(EndCondition::JsonPathExists(JsonPath(vec![
Key("choices"),
Index(0),
Key("finish_reason"),
])))
.usage_paths(UsagePaths {
input_tokens: JsonPath(vec![Key("usage"), Key("prompt_tokens")]),
output_tokens: JsonPath(vec![Key("usage"), Key("completion_tokens")]),
total_tokens: Some(JsonPath(vec![Key("usage"), Key("total_tokens")])),
cache_read_tokens: None,
cache_write_tokens: None,
})
}
pub fn anthropic() -> SseParserBuilder {
use JsonPathSegment::*;
SseParserBuilder::new()
.event_type_prefix("event: ")
.token_path(JsonPath(vec![Key("delta"), Key("text")]))
.end_condition(EndCondition::JsonPathEquals {
path: JsonPath(vec![Key("type")]),
value: "message_stop",
})
.end_condition(EndCondition::JsonPathExists(JsonPath(vec![Key("error")])))
.usage_paths(UsagePaths {
input_tokens: JsonPath(vec![Key("message"), Key("usage"), Key("input_tokens")]),
output_tokens: JsonPath(vec![Key("usage"), Key("output_tokens")]),
total_tokens: None,
cache_read_tokens: Some(JsonPath(vec![
Key("message"),
Key("usage"),
Key("cache_read_input_tokens"),
])),
cache_write_tokens: Some(JsonPath(vec![
Key("message"),
Key("usage"),
Key("cache_creation_input_tokens"),
])),
})
}
pub fn google() -> SseParserBuilder {
use JsonPathSegment::*;
SseParserBuilder::new()
.line_prefix("data: ")
.token_path(JsonPath(vec![
Key("candidates"),
Index(0),
Key("content"),
Key("parts"),
Index(0),
Key("text"),
]))
.end_condition(EndCondition::JsonPathExists(JsonPath(vec![
Key("candidates"),
Index(0),
Key("finishReason"),
])))
.end_condition(EndCondition::JsonPathExists(JsonPath(vec![Key("error")])))
.usage_paths(UsagePaths {
input_tokens: JsonPath(vec![Key("usageMetadata"), Key("promptTokenCount")]),
output_tokens: JsonPath(vec![Key("usageMetadata"), Key("candidatesTokenCount")]),
total_tokens: Some(JsonPath(vec![Key("usageMetadata"), Key("totalTokenCount")])),
cache_read_tokens: None,
cache_write_tokens: None,
})
}
}
pub mod response_extractor {
use super::*;
#[derive(Debug, Clone)]
pub struct ResponseExtractorBuilder {
pub success_codes: Vec<u16>,
pub content_path: String,
pub role_path: Option<String>,
pub finish_reason_path: Option<String>,
pub message_id_path: Option<String>,
pub model_path: Option<String>,
pub usage_paths: Option<UsageExtractPaths>,
pub tool_calls_path: Option<String>,
pub error_paths: HashMap<String, String>,
pub finish_reason_map: HashMap<String, String>,
pub role_map: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct UsageExtractPaths {
pub input: String,
pub output: String,
pub total: Option<String>,
pub cache_read: Option<String>,
pub cache_creation: Option<String>,
}
impl ResponseExtractorBuilder {
pub fn new() -> Self {
Self {
success_codes: vec![200],
content_path: String::new(),
role_path: None,
finish_reason_path: None,
message_id_path: None,
model_path: None,
usage_paths: None,
tool_calls_path: None,
error_paths: HashMap::new(),
finish_reason_map: HashMap::new(),
role_map: HashMap::new(),
}
}
pub fn success_codes(mut self, codes: Vec<u16>) -> Self {
self.success_codes = codes;
self
}
pub fn content_path(mut self, path: impl Into<String>) -> Self {
self.content_path = path.into();
self
}
pub fn role_path(mut self, path: impl Into<String>) -> Self {
self.role_path = Some(path.into());
self
}
pub fn finish_reason_path(mut self, path: impl Into<String>) -> Self {
self.finish_reason_path = Some(path.into());
self
}
pub fn message_id_path(mut self, path: impl Into<String>) -> Self {
self.message_id_path = Some(path.into());
self
}
pub fn model_path(mut self, path: impl Into<String>) -> Self {
self.model_path = Some(path.into());
self
}
pub fn usage_paths(mut self, paths: UsageExtractPaths) -> Self {
self.usage_paths = Some(paths);
self
}
pub fn tool_calls_path(mut self, path: impl Into<String>) -> Self {
self.tool_calls_path = Some(path.into());
self
}
pub fn error_path(mut self, key: impl Into<String>, path: impl Into<String>) -> Self {
self.error_paths.insert(key.into(), path.into());
self
}
pub fn finish_reason_mapping(
mut self,
from: impl Into<String>,
to: impl Into<String>,
) -> Self {
self.finish_reason_map.insert(from.into(), to.into());
self
}
pub fn role_mapping(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.role_map.insert(from.into(), to.into());
self
}
pub fn build(&self) -> ResponseConfig {
let usage = self.usage_paths.as_ref().map(|u| UsageExtract {
input: u.input.clone(),
output: u.output.clone(),
total: u.total.clone(),
});
ResponseConfig {
success_codes: self.success_codes.clone(),
extract: ResponseExtract {
content: self.content_path.clone(),
role: self.role_path.clone(),
finish_reason: self.finish_reason_path.clone(),
usage,
},
error: self.error_paths.clone(),
}
}
}
impl Default for ResponseExtractorBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn openai() -> ResponseExtractorBuilder {
ResponseExtractorBuilder::new()
.success_codes(vec![200])
.content_path("choices[0].message.content")
.role_path("choices[0].message.role")
.finish_reason_path("choices[0].finish_reason")
.message_id_path("id")
.model_path("model")
.usage_paths(UsageExtractPaths {
input: "usage.prompt_tokens".to_string(),
output: "usage.completion_tokens".to_string(),
total: Some("usage.total_tokens".to_string()),
cache_read: None,
cache_creation: None,
})
.tool_calls_path("choices[0].message.tool_calls")
.error_path("message", "error.message")
.error_path("type", "error.type")
.error_path("code", "error.code")
.error_path("param", "error.param")
.finish_reason_mapping("stop", "stop")
.finish_reason_mapping("length", "length")
.finish_reason_mapping("tool_calls", "tool_calls")
.finish_reason_mapping("content_filter", "content_filter")
}
pub fn anthropic() -> ResponseExtractorBuilder {
ResponseExtractorBuilder::new()
.success_codes(vec![200])
.content_path("content[0].text")
.role_path("role")
.finish_reason_path("stop_reason")
.message_id_path("id")
.model_path("model")
.usage_paths(UsageExtractPaths {
input: "usage.input_tokens".to_string(),
output: "usage.output_tokens".to_string(),
total: None,
cache_read: Some("usage.cache_read_input_tokens".to_string()),
cache_creation: Some("usage.cache_creation_input_tokens".to_string()),
})
.tool_calls_path("content[?(@.type==\"tool_use\")]")
.error_path("message", "error.message")
.error_path("type", "error.type")
.error_path("code", "error.code")
.finish_reason_mapping("end_turn", "stop")
.finish_reason_mapping("max_tokens", "length")
.finish_reason_mapping("stop_sequence", "stop")
.finish_reason_mapping("tool_use", "tool_calls")
}
pub fn google() -> ResponseExtractorBuilder {
ResponseExtractorBuilder::new()
.success_codes(vec![200])
.content_path("candidates[0].content.parts[0].text")
.role_path("candidates[0].content.role")
.finish_reason_path("candidates[0].finishReason")
.usage_paths(UsageExtractPaths {
input: "usageMetadata.promptTokenCount".to_string(),
output: "usageMetadata.candidatesTokenCount".to_string(),
total: Some("usageMetadata.totalTokenCount".to_string()),
cache_read: None,
cache_creation: None,
})
.tool_calls_path("candidates[0].content.parts[?(@.functionCall)]")
.error_path("message", "error.message")
.error_path("type", "error.code")
.error_path("code", "error.status")
.finish_reason_mapping("STOP", "stop")
.finish_reason_mapping("MAX_TOKENS", "length")
.finish_reason_mapping("SAFETY", "content_filter")
.finish_reason_mapping("RECITATION", "content_filter")
.finish_reason_mapping("OTHER", "other")
.role_mapping("model", "assistant")
}
}
pub use message_format::openai as openai_message_format;
pub use message_format::anthropic as anthropic_message_format;
pub use message_format::google as google_message_format;
pub use sse_parser::openai as openai_sse_parser;
pub use sse_parser::anthropic as anthropic_sse_parser;
pub use sse_parser::google as google_sse_parser;
pub use response_extractor::openai as openai_response_extractor;
pub use response_extractor::anthropic as anthropic_response_extractor;
pub use response_extractor::google as google_response_extractor;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openai_message_format_builds() {
let builder = message_format::openai();
let config = builder.build().expect("OpenAI message format should build");
assert_eq!(config.name, "openai");
}
#[test]
fn test_anthropic_message_format_builds() {
let builder = message_format::anthropic();
let config = builder.build().expect("Anthropic message format should build");
assert_eq!(config.name, "anthropic");
}
#[test]
fn test_google_message_format_builds() {
let builder = message_format::google();
let config = builder.build().expect("Google message format should build");
assert_eq!(config.name, "google");
}
#[test]
fn test_openai_sse_parser_builds() {
let builder = sse_parser::openai();
let extractor = builder.build_extractor();
let json: serde_json::Value = serde_json::json!({
"choices": [{
"delta": {
"content": "Hello"
}
}]
});
use crate::streaming::sse::TokenExtractor;
match extractor.extract_token(&json) {
crate::streaming::sse::TokenExtractionResult::Token(s) => {
assert_eq!(s, "Hello");
}
_ => panic!("Expected token extraction"),
}
}
#[test]
fn test_anthropic_sse_parser_builds() {
let builder = sse_parser::anthropic();
let extractor = builder.build_extractor();
let json: serde_json::Value = serde_json::json!({
"delta": {
"text": "Hello"
},
"type": "content_block_delta"
});
use crate::streaming::sse::TokenExtractor;
match extractor.extract_token(&json) {
crate::streaming::sse::TokenExtractionResult::Token(s) => {
assert_eq!(s, "Hello");
}
_ => panic!("Expected token extraction"),
}
}
#[test]
fn test_google_sse_parser_builds() {
let builder = sse_parser::google();
let extractor = builder.build_extractor();
let json: serde_json::Value = serde_json::json!({
"candidates": [{
"content": {
"parts": [{
"text": "Hello"
}]
}
}]
});
use crate::streaming::sse::TokenExtractor;
match extractor.extract_token(&json) {
crate::streaming::sse::TokenExtractionResult::Token(s) => {
assert_eq!(s, "Hello");
}
_ => panic!("Expected token extraction"),
}
}
#[test]
fn test_openai_response_extractor_builds() {
let builder = response_extractor::openai();
let config = builder.build();
assert_eq!(config.success_codes, vec![200]);
assert_eq!(config.extract.content, "choices[0].message.content");
}
#[test]
fn test_anthropic_response_extractor_builds() {
let builder = response_extractor::anthropic();
let config = builder.build();
assert_eq!(config.success_codes, vec![200]);
assert_eq!(config.extract.content, "content[0].text");
}
#[test]
fn test_google_response_extractor_builds() {
let builder = response_extractor::google();
let config = builder.build();
assert_eq!(config.success_codes, vec![200]);
assert_eq!(config.extract.content, "candidates[0].content.parts[0].text");
}
#[test]
fn test_openai_sse_end_detection() {
let builder = sse_parser::openai();
let extractor = builder.build_extractor();
use crate::streaming::sse::TokenExtractor;
assert!(extractor.is_end_payload("[DONE]"));
assert!(!extractor.is_end_payload("data: {}"));
let end_json: serde_json::Value = serde_json::json!({
"choices": [{
"delta": {},
"finish_reason": "stop"
}]
});
match extractor.extract_token(&end_json) {
crate::streaming::sse::TokenExtractionResult::EndStream => {}
_ => panic!("Expected end stream detection"),
}
}
#[test]
fn test_anthropic_sse_end_detection() {
let builder = sse_parser::anthropic();
let extractor = builder.build_extractor();
use crate::streaming::sse::TokenExtractor;
let end_json: serde_json::Value = serde_json::json!({
"type": "message_stop"
});
match extractor.extract_token(&end_json) {
crate::streaming::sse::TokenExtractionResult::EndStream => {}
_ => panic!("Expected end stream detection"),
}
}
#[test]
fn test_google_sse_end_detection() {
let builder = sse_parser::google();
let extractor = builder.build_extractor();
use crate::streaming::sse::TokenExtractor;
let end_json: serde_json::Value = serde_json::json!({
"candidates": [{
"content": {
"parts": [{
"text": ""
}]
},
"finishReason": "STOP"
}]
});
match extractor.extract_token(&end_json) {
crate::streaming::sse::TokenExtractionResult::EndStream => {}
_ => panic!("Expected end stream detection"),
}
}
}