diff --git a/src/main.rs b/src/main.rs index fe3aa73..72991c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -249,11 +249,11 @@ async fn autocomplete_model_id<'a>( #[poise::command(slash_command)] async fn model( ctx: Context<'_>, - #[description = "Exact model ID from /models or /status"] + #[description = "Exact model ID from /free-models or /status"] #[autocomplete = "autocomplete_model_id"] id: String, #[description = "Hide the description (default false)"] compact: Option, - #[description = "Include raw pricing (default true)"] show_pricing: Option, + #[description = "Include pricing per 1M tokens (default true)"] show_pricing: Option, #[description = "Show details to everyone (default false)"] public: Option, ) -> Result<(), Error> { let public = public.unwrap_or(false); diff --git a/src/presentation.rs b/src/presentation.rs index 612978b..40ef01d 100644 --- a/src/presentation.rs +++ b/src/presentation.rs @@ -173,6 +173,56 @@ pub fn snapshot(state: &State, options: &Browse, poll_seconds: u64) -> serenity: .field("Poll interval", format!("{formatted_poll} seconds ยท run /status again to refresh this snapshot"), false) } +fn format_price_usd(val: f64) -> String { + if val == 0.0 { + return "$0.00".into(); + } + let formatted = format!("{val:.4}"); + let trimmed = formatted.trim_end_matches('0'); + let result = if trimmed.ends_with('.') { + format!("{trimmed}00") + } else { + let decimals = trimmed.split('.').nth(1).unwrap_or("").len(); + if decimals < 2 { + format!("{trimmed}0") + } else { + trimmed.to_string() + } + }; + format!("${result}") +} + +pub fn format_price_per_1m(key: &str, raw: &str) -> String { + if let Ok(price) = raw.parse::() { + if price < 0.0 { + return "Variable / unknown".into(); + } + if key == "request" { + if price == 0.0 { + "$0 / request".into() + } else { + format!("${price:.4} / request") + } + } else if key == "image" { + if price == 0.0 { + "$0 / image".into() + } else { + format!("${price:.4} / image") + } + } else { + let per_1m = price * 1_000_000.0; + let formatted = format_price_usd(per_1m); + if per_1m == 0.0 { + format!("{formatted} / 1M tokens (Free)") + } else { + format!("{formatted} / 1M tokens") + } + } + } else { + raw.to_string() + } +} + pub fn model_details(model: &Model, compact: bool, show_pricing: bool) -> serenity::CreateEmbed { let mut result = embed(&model.name) .field("Model ID", text(&model.id, 1024), false) @@ -206,11 +256,17 @@ pub fn model_details(model: &Model, compact: bool, show_pricing: bool) -> sereni .pricing .iter() .take(10) - .map(|(key, value)| format!("{}: {}", text(key, 40), text(value, 45))) + .map(|(key, value)| { + format!( + "{}: {}", + text(key, 40), + text(&format_price_per_1m(key, value), 45) + ) + }) .collect::>() .join("\n"); result = result.field( - "Raw USD prices (prompt/completion per token)", + "Pricing (USD per 1M tokens)", if prices.is_empty() { "Unavailable" } else { @@ -411,4 +467,35 @@ pub mod tests { assert_eq!(format_number(1000000), "1,000,000"); assert_eq!(format_number(u64::MAX), "18,446,744,073,709,551,615"); } + + #[test] + fn format_price_per_1m_cases() { + assert_eq!( + format_price_per_1m("prompt", "0"), + "$0.00 / 1M tokens (Free)" + ); + assert_eq!( + format_price_per_1m("prompt", "0.00000015"), + "$0.15 / 1M tokens" + ); + assert_eq!( + format_price_per_1m("completion", "0.0000015"), + "$1.50 / 1M tokens" + ); + assert_eq!( + format_price_per_1m("prompt", "0.000003"), + "$3.00 / 1M tokens" + ); + assert_eq!( + format_price_per_1m("prompt", "0.000000075"), + "$0.075 / 1M tokens" + ); + assert_eq!(format_price_per_1m("request", "0"), "$0 / request"); + assert_eq!(format_price_per_1m("image", "0.001"), "$0.0010 / image"); + assert_eq!(format_price_per_1m("prompt", "-1"), "Variable / unknown"); + assert_eq!( + format_price_per_1m("overrides", "tiered pricing"), + "tiered pricing" + ); + } }