330 lines
11 KiB
Rust
330 lines
11 KiB
Rust
use crate::{catalog::Model, serenity, store::State};
|
|
|
|
pub fn no_mentions() -> serenity::CreateAllowedMentions {
|
|
serenity::CreateAllowedMentions::new()
|
|
.all_users(false)
|
|
.all_roles(false)
|
|
.everyone(false)
|
|
.replied_user(false)
|
|
}
|
|
|
|
pub fn text(value: &str, limit: usize) -> String {
|
|
if limit == 0 {
|
|
return String::new();
|
|
}
|
|
let mut result = String::new();
|
|
let mut length = 0;
|
|
for ch in value.chars().filter(|ch| !ch.is_control() || *ch == '\n') {
|
|
let escape = matches!(
|
|
ch,
|
|
'\\' | '`' | '*' | '_' | '~' | '[' | ']' | '<' | '>' | '|' | '#' | '-'
|
|
);
|
|
let width = ch.len_utf16() + usize::from(escape);
|
|
if length + width > limit.saturating_sub(1) {
|
|
result.push('…');
|
|
break;
|
|
}
|
|
if escape {
|
|
result.push('\\');
|
|
}
|
|
result.push(ch);
|
|
length += width;
|
|
}
|
|
if result.is_empty() {
|
|
"—".into()
|
|
} else {
|
|
result
|
|
}
|
|
}
|
|
|
|
pub fn embed(title: &str) -> serenity::CreateEmbed {
|
|
serenity::CreateEmbed::new()
|
|
.title(text(title, 256))
|
|
.color(0x8367ef)
|
|
.footer(serenity::CreateEmbedFooter::new(
|
|
"Model Radar • Catalog prices, not unlimited access • Limits may apply",
|
|
))
|
|
}
|
|
|
|
pub struct Browse {
|
|
pub free_only: bool,
|
|
pub search: String,
|
|
pub min_context: u32,
|
|
pub page: u32,
|
|
pub page_size: u32,
|
|
pub compact: bool,
|
|
}
|
|
|
|
impl Default for Browse {
|
|
fn default() -> Self {
|
|
Self {
|
|
free_only: true,
|
|
search: String::new(),
|
|
min_context: 0,
|
|
page: 1,
|
|
page_size: 10,
|
|
compact: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn model_line(model: &Model, compact: bool) -> String {
|
|
let pricing = if model.is_free() {
|
|
"Free"
|
|
} else {
|
|
"Paid / unknown"
|
|
};
|
|
if compact {
|
|
format!(
|
|
"{} · {} tokens · {pricing}",
|
|
text(&model.id, 220),
|
|
model.context_length
|
|
)
|
|
} else {
|
|
format!(
|
|
"**{}**\n{} · {} tokens · {pricing}",
|
|
text(&model.name, 85),
|
|
text(&model.id, 220),
|
|
model.context_length
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(state: &State, options: &Browse, poll_seconds: u64) -> serenity::CreateEmbed {
|
|
let query = options.search.to_lowercase();
|
|
let matches: Vec<_> = state
|
|
.catalog
|
|
.values()
|
|
.filter(|model| {
|
|
(!options.free_only || model.is_free())
|
|
&& model.context_length >= u64::from(options.min_context)
|
|
&& (model.id.to_lowercase().contains(&query)
|
|
|| model.name.to_lowercase().contains(&query))
|
|
})
|
|
.collect();
|
|
let size = options.page_size.clamp(1, 10) as usize;
|
|
let pages = matches.len().div_ceil(size).max(1);
|
|
let page = (options.page as usize).clamp(1, pages);
|
|
let lines = matches
|
|
.iter()
|
|
.skip((page - 1) * size)
|
|
.take(size)
|
|
.map(|model| model_line(model, options.compact))
|
|
.collect::<Vec<_>>()
|
|
.join("\n\n");
|
|
let freshness = state
|
|
.last_success
|
|
.map(|time| format!("Last successful catalog fetch: <t:{time}:f> (<t:{time}:R>)."))
|
|
.unwrap_or_else(|| "No successful catalog fetch recorded.".into());
|
|
embed("Catalog snapshot")
|
|
.description(format!("Snapshot only; not live updating. Catalog freshness, not bot uptime or health.\n{freshness}\n\n{}", if lines.is_empty() { "No matching models." } else { &lines }))
|
|
.field("Catalog", format!("{} total · {} free · {} matching", state.catalog.len(), state.catalog.values().filter(|model| model.is_free()).count(), matches.len()), false)
|
|
.field("Filters", format!("Scope: {}\nSearch: {}\nMinimum context: {} tokens\nCompact: {}", if options.free_only { "Free only" } else { "All models" }, text(&options.search, 240), options.min_context, options.compact), false)
|
|
.field("Page", format!("{page}/{pages} · {size} per page"), true)
|
|
.field("Poll interval", format!("{poll_seconds} seconds · run /status again to refresh this snapshot"), false)
|
|
}
|
|
|
|
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)
|
|
.field("Context", format!("{} tokens", model.context_length), true)
|
|
.field(
|
|
"Free catalog pricing",
|
|
if model.is_free() {
|
|
"Yes"
|
|
} else {
|
|
"No / unknown"
|
|
},
|
|
true,
|
|
);
|
|
if !compact {
|
|
result = result.description(text(&model.description, 2000));
|
|
}
|
|
if show_pricing {
|
|
let prices = model
|
|
.pricing
|
|
.iter()
|
|
.take(10)
|
|
.map(|(key, value)| format!("{}: {}", text(key, 40), text(value, 45)))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
result = result.field(
|
|
"Raw USD prices (prompt/completion per token)",
|
|
if prices.is_empty() {
|
|
"Unavailable"
|
|
} else {
|
|
&prices
|
|
},
|
|
false,
|
|
);
|
|
}
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
use super::*;
|
|
use std::collections::BTreeMap;
|
|
|
|
pub fn assert_embed_bounds(value: &serde_json::Value) {
|
|
let units =
|
|
|value: &serde_json::Value| value.as_str().unwrap_or_default().encode_utf16().count();
|
|
assert!(units(&value["title"]) <= 256);
|
|
assert!(units(&value["description"]) <= 4096);
|
|
assert!(units(&value["footer"]["text"]) <= 2048);
|
|
let mut total =
|
|
units(&value["title"]) + units(&value["description"]) + units(&value["footer"]["text"]);
|
|
if let Some(fields) = value["fields"].as_array() {
|
|
assert!(fields.len() <= 25);
|
|
for field in fields {
|
|
assert!((1..=256).contains(&units(&field["name"])));
|
|
assert!((1..=1024).contains(&units(&field["value"])));
|
|
total += units(&field["name"]) + units(&field["value"]);
|
|
}
|
|
}
|
|
assert!(total <= 6000, "embed has {total} UTF-16 units");
|
|
}
|
|
|
|
fn model(id: &str, free: bool, context: u64) -> Model {
|
|
Model {
|
|
id: id.into(),
|
|
name: "Example".into(),
|
|
description: "Description".into(),
|
|
context_length: context,
|
|
pricing: BTreeMap::from([
|
|
("prompt".into(), if free { "0" } else { "1" }.into()),
|
|
("completion".into(), "0".into()),
|
|
]),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn markdown_escaping_preserves_id_characters() {
|
|
assert_eq!(
|
|
text("vendor/model_under`tick", 1024),
|
|
"vendor/model\\_under\\`tick"
|
|
);
|
|
assert_eq!(text("a\\b[*]", 1024), "a\\\\b\\[\\*\\]");
|
|
let payload = serde_json::to_value(model_details(
|
|
&model("vendor/model_under`tick", true, 8192),
|
|
false,
|
|
true,
|
|
))
|
|
.unwrap();
|
|
assert_eq!(payload["fields"][0]["value"], "vendor/model\\_under\\`tick");
|
|
assert_embed_bounds(&payload);
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_lengths_and_all_payloads_are_bounded() {
|
|
let large = "\u{1d11e}_`\\[]".repeat(2000);
|
|
for limit in [0, 1, 2, 3, 40, 256, 1024, 2000] {
|
|
assert!(text(&large, limit).encode_utf16().count() <= limit);
|
|
}
|
|
let mut example = model(&large, false, u64::MAX);
|
|
example.name = large.clone();
|
|
example.description = large.clone();
|
|
example.pricing = (0..30)
|
|
.map(|index| (format!("{index}{large}"), large.clone()))
|
|
.collect();
|
|
for compact in [false, true] {
|
|
for show_pricing in [false, true] {
|
|
let value =
|
|
serde_json::to_value(model_details(&example, compact, show_pricing)).unwrap();
|
|
assert_embed_bounds(&value);
|
|
assert_eq!(
|
|
value["fields"].as_array().unwrap().len(),
|
|
if show_pricing { 4 } else { 3 }
|
|
);
|
|
assert_eq!(value.get("description").is_none(), compact);
|
|
}
|
|
let state = State {
|
|
catalog: (0..30)
|
|
.map(|index| (index.to_string(), example.clone()))
|
|
.collect(),
|
|
last_success: Some(u64::MAX),
|
|
..Default::default()
|
|
};
|
|
for page_size in [0, 1, 10, u32::MAX] {
|
|
let options = Browse {
|
|
free_only: false,
|
|
compact,
|
|
page_size,
|
|
..Default::default()
|
|
};
|
|
assert_embed_bounds(
|
|
&serde_json::to_value(snapshot(&state, &options, 86400)).unwrap(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_filters_pagination_and_freshness() {
|
|
let state = State {
|
|
catalog: [
|
|
model("a/free", true, 100),
|
|
model("b/paid", false, 200),
|
|
model("c/free", true, 50),
|
|
]
|
|
.into_iter()
|
|
.map(|model| (model.id.clone(), model))
|
|
.collect(),
|
|
last_success: Some(123),
|
|
..Default::default()
|
|
};
|
|
let value = serde_json::to_value(snapshot(&state, &Browse::default(), 60)).unwrap();
|
|
let description = value["description"].as_str().unwrap();
|
|
assert!(description.contains("not live updating"));
|
|
assert!(description.contains("not bot uptime or health"));
|
|
assert!(description.contains("<t:123:f>"));
|
|
assert!(!description.contains("b/paid"));
|
|
assert!(description.contains("a/free") && description.contains("c/free"));
|
|
let options = Browse {
|
|
free_only: false,
|
|
search: "PAID".into(),
|
|
min_context: 200,
|
|
page: u32::MAX,
|
|
page_size: 1,
|
|
compact: true,
|
|
};
|
|
let value = serde_json::to_value(snapshot(&state, &options, 75)).unwrap();
|
|
assert!(value["description"].as_str().unwrap().contains("b/paid"));
|
|
assert_eq!(value["fields"][2]["value"], "1/1 · 1 per page");
|
|
assert!(
|
|
value["fields"][3]["value"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("75 seconds")
|
|
);
|
|
let options = Browse {
|
|
min_context: 201,
|
|
..options
|
|
};
|
|
let value = serde_json::to_value(snapshot(&state, &options, 600)).unwrap();
|
|
assert!(
|
|
value["description"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("No matching models.")
|
|
);
|
|
let value =
|
|
serde_json::to_value(snapshot(&State::default(), &Browse::default(), 600)).unwrap();
|
|
assert!(
|
|
value["description"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("No successful catalog fetch")
|
|
);
|
|
let options = Browse {
|
|
free_only: false,
|
|
page_size: 1,
|
|
page: 2,
|
|
..Default::default()
|
|
};
|
|
let value = serde_json::to_value(snapshot(&state, &options, 600)).unwrap();
|
|
assert!(value["description"].as_str().unwrap().contains("b/paid"));
|
|
assert!(!value["description"].as_str().unwrap().contains("a/free"));
|
|
assert_eq!(value["fields"][2]["value"], "2/3 · 1 per page");
|
|
}
|
|
}
|