use std::future::Future;
use std::pin::Pin;
use crate::context::CommandContext;
use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
pub fn parse_token_budget(arg: &str) -> Result<Option<u32>, String> {
let trimmed = arg.trim();
if trimmed.is_empty() {
return Err("empty token budget — expected a number (e.g. 8k, 1M, 0, off)".to_owned());
}
if trimmed.eq_ignore_ascii_case("off") {
return Ok(None);
}
let (numeric, multiplier) = match trimmed.chars().last() {
Some(c) if c.eq_ignore_ascii_case(&'k') => (&trimmed[..trimmed.len() - 1], 1_000.0),
Some(c) if c.eq_ignore_ascii_case(&'m') => (&trimmed[..trimmed.len() - 1], 1_000_000.0),
_ => (trimmed, 1.0),
};
if numeric.is_empty() {
return Err(format!(
"'{trimmed}' is missing a numeric value before the suffix"
));
}
let value: f64 = numeric
.parse()
.map_err(|_| format!("'{trimmed}' is not a valid token budget"))?;
if value.is_sign_negative() {
return Err(format!("token budget must not be negative: '{trimmed}'"));
}
let scaled = value * multiplier;
if !scaled.is_finite() || scaled > f64::from(u32::MAX) {
return Err(format!("'{trimmed}' is too large for a token budget"));
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rounded = scaled.round() as u32;
if rounded == 0 {
return Ok(None);
}
Ok(Some(rounded))
}
pub struct ThinkTokensCommand;
impl CommandHandler<CommandContext<'_>> for ThinkTokensCommand {
fn name(&self) -> &'static str {
"/think-tokens"
}
fn description(&self) -> &'static str {
"Show or set the active provider's runtime thinking-token budget"
}
fn args_hint(&self) -> &'static str {
"[N|Nk|NM|off]"
}
fn category(&self) -> SlashCategory {
SlashCategory::Configuration
}
fn requires_auth(&self) -> bool {
true
}
fn handle<'a>(
&'a self,
ctx: &'a mut CommandContext<'_>,
args: &'a str,
) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
use tracing::Instrument as _;
let span = tracing::info_span!("commands.think_tokens.handle");
Box::pin(
async move {
let result = ctx.agent.handle_think_tokens(args).await;
Ok(CommandOutput::message_or_silent(result))
}
.instrument(span),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
use crate::sink::NullSink;
use std::assert_matches;
#[test]
fn think_tokens_name_and_description() {
assert_eq!(ThinkTokensCommand.name(), "/think-tokens");
assert!(!ThinkTokensCommand.description().is_empty());
}
#[tokio::test]
async fn think_tokens_returns_silent_when_agent_returns_empty() {
let mut sink = NullSink;
let mut debug = MockDebug;
let mut messages = MockMessages;
let session = MockSession;
let mut agent = crate::NullAgent;
let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
let out = ThinkTokensCommand.handle(&mut ctx, "").await.unwrap();
assert_matches!(out, CommandOutput::Silent);
}
#[test]
fn parse_token_budget_empty_is_error() {
assert!(parse_token_budget("").is_err());
assert!(parse_token_budget(" ").is_err());
}
#[test]
fn parse_token_budget_bare_k_is_error() {
assert!(parse_token_budget("k").is_err());
}
#[test]
fn parse_token_budget_negative_is_error() {
assert!(parse_token_budget("-1").is_err());
}
#[test]
fn parse_token_budget_malformed_compound_is_error() {
assert!(parse_token_budget("1.2.3k").is_err());
}
#[test]
fn parse_token_budget_off_disables() {
assert_eq!(parse_token_budget("off"), Ok(None));
assert_eq!(parse_token_budget("OFF"), Ok(None));
assert_eq!(parse_token_budget("Off"), Ok(None));
}
#[test]
fn parse_token_budget_zero_disables() {
assert_eq!(parse_token_budget("0"), Ok(None));
}
#[test]
fn parse_token_budget_k_suffix() {
assert_eq!(parse_token_budget("8k"), Ok(Some(8_000)));
assert_eq!(parse_token_budget("8K"), Ok(Some(8_000)));
}
#[test]
fn parse_token_budget_decimal_k_suffix_rounds() {
assert_eq!(parse_token_budget("10.5k"), Ok(Some(10_500)));
}
#[test]
fn parse_token_budget_m_suffix() {
assert_eq!(parse_token_budget("1M"), Ok(Some(1_000_000)));
assert_eq!(parse_token_budget("1m"), Ok(Some(1_000_000)));
}
#[test]
fn parse_token_budget_bare_integer() {
assert_eq!(parse_token_budget("1024"), Ok(Some(1_024)));
}
#[test]
fn parse_token_budget_overflow_is_error() {
assert!(parse_token_budget("999999999999999M").is_err());
}
#[test]
fn parse_token_budget_trims_whitespace() {
assert_eq!(parse_token_budget(" 8k "), Ok(Some(8_000)));
}
}