feat: add presentation formatting, filter options, and testping command

This commit is contained in:
katR 2026-09-17 12:11:12 +05:30
parent 34d54b5c45
commit 6ffdb4ca4f
4 changed files with 1327 additions and 245 deletions

View file

@ -7,7 +7,7 @@ use crate::Error;
const ENDPOINT: &str = "https://openrouter.ai/api/v1/models";
const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Model {
pub id: String,
pub name: String,
@ -45,7 +45,7 @@ where
.collect()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Event {
pub model: Model,
pub kind: String,
@ -96,7 +96,7 @@ impl Model {
}
}
fn validate(catalog: &BTreeMap<String, Model>) -> Result<(), Error> {
pub(crate) fn validate(catalog: &BTreeMap<String, Model>) -> Result<(), Error> {
if catalog.is_empty() {
return Err("catalog is empty".into());
}
@ -353,4 +353,11 @@ mod tests {
fn validate_rejects_empty_catalog() {
assert!(validate(&BTreeMap::new()).is_err());
}
#[test]
fn validate_requires_keys_to_match_model_ids() {
let model = model("example/model", &[("prompt", "0"), ("completion", "0")]);
assert!(validate(&BTreeMap::from([("wrong/key".into(), model.clone())])).is_err());
assert!(validate(&BTreeMap::from([(model.id.clone(), model)])).is_ok());
}
}

File diff suppressed because it is too large Load diff

330
src/presentation.rs Normal file
View file

@ -0,0 +1,330 @@
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");
}
}

View file

@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use serde::{Deserialize, Serialize};
use crate::Error;
use crate::catalog::validate as validate_catalog;
use crate::catalog::{Event, Model};
static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
@ -18,14 +19,57 @@ pub struct State {
pub last_success: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Subscription {
pub channel_id: u64,
pub role_id: Option<u64>,
pub free_only: bool,
#[serde(default)]
pub search: String,
#[serde(default)]
pub min_context: u32,
#[serde(default = "default_true")]
pub ping_enabled: bool,
#[serde(default)]
pub compact: bool,
pub pending: Vec<Event>,
}
fn default_true() -> bool {
true
}
impl Subscription {
pub fn matches(&self, model: &Model) -> bool {
if (self.free_only && !model.is_free())
|| model.context_length < u64::from(self.min_context)
{
return false;
}
let search = self.search.trim().to_lowercase();
search.is_empty()
|| model.name.to_lowercase().contains(&search)
|| model.id.to_lowercase().contains(&search)
}
}
fn validate_ids(state: &State) -> Result<(), Error> {
for (guild, sub) in &state.subscriptions {
if *guild == 0 || sub.channel_id == 0 {
return Err("subscription IDs must be nonzero".into());
}
if let Some(role) = sub.role_id {
if role == 0 {
return Err("subscription role ID must be nonzero".into());
}
if role == *guild {
return Err("subscription role ID must differ from guild ID".into());
}
}
}
Ok(())
}
pub struct Store {
pub state: State,
path: PathBuf,
@ -33,11 +77,15 @@ pub struct Store {
impl Store {
pub async fn load(path: PathBuf) -> Result<Self, Error> {
let state = match tokio::fs::read(&path).await {
let state: State = match tokio::fs::read(&path).await {
Ok(bytes) => serde_json::from_slice(&bytes)?,
Err(error) if error.kind() == ErrorKind::NotFound => State::default(),
Err(error) => return Err(error.into()),
};
if state.initialized || !state.catalog.is_empty() {
validate_catalog(&state.catalog)?;
}
validate_ids(&state)?;
Ok(Self { state, path })
}
@ -134,6 +182,10 @@ mod tests {
channel_id: 42,
role_id: Some(99),
free_only: true,
search: " Example ".into(),
min_context: 8192,
ping_enabled: false,
compact: true,
pending: vec![Event {
model,
kind: "new".into(),
@ -146,6 +198,10 @@ mod tests {
channel_id: 43,
role_id: None,
free_only: false,
search: String::new(),
min_context: 0,
ping_enabled: true,
compact: false,
pending: Vec::new(),
},
),
@ -186,6 +242,157 @@ mod tests {
assert_eq!(std::fs::read_dir(&directory.0).unwrap().count(), 1);
}
#[tokio::test]
async fn legacy_states_preserve_pending_and_default_settings() {
let directory = TestDirectory::new();
let path = directory.state_path();
let mut expected = populated_state();
let mut legacy = serde_json::to_value(&expected).unwrap();
for sub in legacy["subscriptions"]
.as_object_mut()
.unwrap()
.values_mut()
{
for field in ["search", "min_context", "ping_enabled", "compact"] {
sub.as_object_mut().unwrap().remove(field);
}
}
for sub in expected.subscriptions.values_mut() {
sub.search.clear();
sub.min_context = 0;
sub.ping_enabled = true;
sub.compact = false;
}
let bytes = serde_json::to_vec(&legacy).unwrap();
let decoded: State = serde_json::from_slice(&bytes).unwrap();
assert_eq!(decoded.subscriptions, expected.subscriptions);
tokio::fs::write(&path, &bytes).await.unwrap();
let mut loaded = Store::load(path.clone()).await.unwrap();
assert_eq!(loaded.state.subscriptions, expected.subscriptions);
assert_eq!(loaded.state.catalog, expected.catalog);
assert_eq!(loaded.state.last_success, expected.last_success);
assert!(loaded.state.initialized);
assert_eq!(tokio::fs::read(&path).await.unwrap(), bytes);
loaded.commit(loaded.state.clone()).await.unwrap();
assert_eq!(
Store::load(path).await.unwrap().state.subscriptions,
expected.subscriptions
);
}
#[test]
fn subscription_filters_match_name_or_id_and_combine_constraints() {
let state = populated_state();
let mut model = state.catalog["example/model"].clone();
model.name = "Friendly ÉXAMPLE".into();
let mut sub = state.subscriptions[&u64::MAX].clone();
for search in ["", " \t\n", " EXAMPLE/MO ", " fRiEnDlY ", "éxample"] {
sub.search = search.into();
assert!(sub.matches(&model), "{search:?}");
}
for search in ["missing", "Description", "friendly example/model"] {
sub.search = search.into();
assert!(!sub.matches(&model), "{search:?}");
}
sub.search = "friendly".into();
sub.min_context = 8193;
assert!(!sub.matches(&model));
sub.min_context = 8192;
assert!(sub.matches(&model));
model.pricing.insert("prompt".into(), "1".into());
assert!(!sub.matches(&model));
sub.free_only = false;
assert!(sub.matches(&model));
sub.search = "missing".into();
assert!(!sub.matches(&model));
sub.search.clear();
model.context_length = 0;
assert!(!sub.matches(&model));
sub.min_context = 0;
assert!(sub.matches(&model));
sub.min_context = u32::MAX;
model.context_length = u64::MAX;
assert!(sub.matches(&model));
model.pricing.clear();
sub.free_only = true;
assert!(!sub.matches(&model));
}
#[tokio::test]
async fn load_rejects_invalid_subscription_ids_without_overwriting() {
let directory = TestDirectory::new();
let path = directory.state_path();
for (guild, channel, role) in [
(0, 42, None),
(1, 0, None),
(1, 42, Some(0)),
(1, 42, Some(1)),
] {
for ping_enabled in [false, true] {
let mut state = populated_state();
let mut sub = state.subscriptions[&u64::MAX].clone();
sub.channel_id = channel;
sub.role_id = role;
sub.ping_enabled = ping_enabled;
state.subscriptions = BTreeMap::from([(guild, sub)]);
let bytes = serde_json::to_vec(&state).unwrap();
tokio::fs::write(&path, &bytes).await.unwrap();
assert!(Store::load(path.clone()).await.is_err());
assert_eq!(tokio::fs::read(&path).await.unwrap(), bytes);
}
}
}
#[tokio::test]
async fn load_validates_catalog_for_initialized_and_uninitialized_states() {
let directory = TestDirectory::new();
let path = directory.state_path();
for initialized in [false, true] {
for invalid in ["empty", "key", "id", "name"] {
let mut state = populated_state();
state.initialized = initialized;
match invalid {
"empty" => state.catalog.clear(),
"key" => {
let model = state.catalog.pop_first().unwrap().1;
state.catalog.insert("wrong/key".into(), model);
}
"id" => {
let mut model = state.catalog.pop_first().unwrap().1;
model.id = " ".into();
state.catalog.insert(model.id.clone(), model);
}
_ => state.catalog.values_mut().next().unwrap().name = " ".into(),
}
let bytes = serde_json::to_vec(&state).unwrap();
tokio::fs::write(&path, &bytes).await.unwrap();
let loaded = Store::load(path.clone()).await;
assert_eq!(loaded.is_ok(), !initialized && invalid == "empty");
assert_eq!(tokio::fs::read(&path).await.unwrap(), bytes);
}
}
}
#[tokio::test]
async fn new_settings_round_trip_preserves_pending_without_filtering() {
let directory = TestDirectory::new();
let path = directory.state_path();
let mut store = Store::load(path.clone()).await.unwrap();
for ping_enabled in [false, true] {
let mut state = populated_state();
let sub = state.subscriptions.get_mut(&u64::MAX).unwrap();
sub.search = " Unmatched ÉXAMPLE ".into();
sub.min_context = u32::MAX;
sub.ping_enabled = ping_enabled;
sub.compact = !ping_enabled;
assert!(!sub.matches(&sub.pending[0].model));
store.commit(state.clone()).await.unwrap();
let loaded = Store::load(path.clone()).await.unwrap();
assert_eq!(loaded.state.subscriptions, state.subscriptions);
assert_eq!(loaded.state.catalog, state.catalog);
}
}
#[tokio::test]
async fn corrupt_files_are_errors_and_not_overwritten() {
let directory = TestDirectory::new();