use anyhow::Result;
use colored::*;
use llm_test_bench_core::providers::CompletionResponse;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum OutputFormat {
Pretty,
Json,
JsonPretty,
Plain,
}
impl Default for OutputFormat {
fn default() -> Self {
Self::Pretty
}
}
pub fn display_response(response: &CompletionResponse, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Pretty => display_pretty(response),
OutputFormat::Json => display_json(response),
OutputFormat::JsonPretty => display_json_pretty(response),
OutputFormat::Plain => display_plain(response),
}
}
fn display_pretty(response: &CompletionResponse) -> Result<()> {
println!();
println!("{}", "━".repeat(80).bright_blue());
println!("{} {}", "Response".bright_blue().bold(), response.id.dimmed());
println!("{}", "━".repeat(80).bright_blue());
println!();
println!("{}", response.content);
println!();
println!("{}", "─".repeat(80).dimmed());
println!("{} {}", "Model:".bright_cyan(), response.model);
println!("{} {}", "Finish Reason:".bright_cyan(), format!("{:?}", response.finish_reason));
println!(
"{} {} prompt + {} completion = {} total",
"Tokens:".bright_cyan(),
response.usage.prompt_tokens.to_string().yellow(),
response.usage.completion_tokens.to_string().yellow(),
response.usage.total_tokens.to_string().green().bold()
);
println!("{} {}", "Created:".bright_cyan(), response.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!("{}", "━".repeat(80).bright_blue());
println!();
Ok(())
}
fn display_json(response: &CompletionResponse) -> Result<()> {
let json = serde_json::to_string(response)?;
println!("{}", json);
Ok(())
}
fn display_json_pretty(response: &CompletionResponse) -> Result<()> {
let json = serde_json::to_string_pretty(response)?;
println!("{}", json);
Ok(())
}
fn display_plain(response: &CompletionResponse) -> Result<()> {
print!("{}", response.content);
Ok(())
}
pub struct StreamingOutput {
format: OutputFormat,
total_chars: usize,
start_time: std::time::Instant,
}
impl StreamingOutput {
pub fn new(format: OutputFormat) -> Self {
Self {
format,
total_chars: 0,
start_time: std::time::Instant::now(),
}
}
pub fn display_header(&self, model: &str) {
match self.format {
OutputFormat::Pretty => {
println!();
println!("{}", "━".repeat(80).bright_blue());
println!("{} {}", "Streaming Response".bright_blue().bold(), model.dimmed());
println!("{}", "━".repeat(80).bright_blue());
println!();
}
OutputFormat::Plain => {} OutputFormat::Json | OutputFormat::JsonPretty => {
print!("{{\"chunks\":[");
}
}
}
pub fn display_chunk(&mut self, content: &str, is_first: bool) -> Result<()> {
self.total_chars += content.len();
match self.format {
OutputFormat::Pretty | OutputFormat::Plain => {
print!("{}", content);
use std::io::Write;
std::io::stdout().flush()?;
}
OutputFormat::Json | OutputFormat::JsonPretty => {
if !is_first {
print!(",");
}
print!("\"{}\"", content.replace('\"', "\\\""));
}
}
Ok(())
}
pub fn display_footer(&self, finish_reason: Option<&str>) {
let elapsed = self.start_time.elapsed();
match self.format {
OutputFormat::Pretty => {
println!("\n");
println!("{}", "─".repeat(80).dimmed());
if let Some(reason) = finish_reason {
println!("{} {}", "Finish Reason:".bright_cyan(), reason);
}
println!(
"{} {} chars in {:.2}s ({:.0} chars/s)",
"Stats:".bright_cyan(),
self.total_chars.to_string().yellow(),
elapsed.as_secs_f64(),
self.total_chars as f64 / elapsed.as_secs_f64().max(0.001)
);
println!("{}", "━".repeat(80).bright_blue());
println!();
}
OutputFormat::Plain => {
}
OutputFormat::Json | OutputFormat::JsonPretty => {
println!("],\"elapsed_ms\":{}}}", elapsed.as_millis());
}
}
}
}
pub fn display_error(error: &str, suggestion: Option<&str>) {
eprintln!();
eprintln!("{} {}", "Error:".red().bold(), error.red());
if let Some(hint) = suggestion {
eprintln!();
eprintln!("{} {}", "Hint:".yellow().bold(), hint.yellow());
}
eprintln!();
}
#[cfg(test)]
mod tests {
use super::*;
use llm_test_bench_core::providers::{FinishReason, TokenUsage};
fn create_test_response() -> CompletionResponse {
CompletionResponse {
id: "test-123".to_string(),
content: "Hello, world!".to_string(),
model: "gpt-4".to_string(),
usage: TokenUsage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
finish_reason: FinishReason::Stop,
created_at: chrono::Utc::now(),
metadata: serde_json::Value::Null,
}
}
#[test]
fn test_display_json() {
let response = create_test_response();
assert!(display_json(&response).is_ok());
}
#[test]
fn test_display_json_pretty() {
let response = create_test_response();
assert!(display_json_pretty(&response).is_ok());
}
#[test]
fn test_display_plain() {
let response = create_test_response();
assert!(display_plain(&response).is_ok());
}
#[test]
fn test_streaming_output() {
let mut output = StreamingOutput::new(OutputFormat::Plain);
output.display_header("gpt-4");
assert!(output.display_chunk("Hello", true).is_ok());
assert!(output.display_chunk(" world", false).is_ok());
output.display_footer(Some("stop"));
}
}