use crate::{OutputFormat, JSON_CONTENT_TYPE, TOON_CONTENT_TYPE};
use http::{header, StatusCode};
use rustapi_core::{ApiError, IntoResponse, Response};
use rustapi_openapi::{
MediaType, Operation, OperationModifier, ResponseModifier, ResponseSpec, SchemaRef,
};
use serde::Serialize;
use std::collections::BTreeMap;
pub const X_TOKEN_COUNT_JSON: &str = "x-token-count-json";
pub const X_TOKEN_COUNT_TOON: &str = "x-token-count-toon";
pub const X_TOKEN_SAVINGS: &str = "x-token-savings";
pub const X_FORMAT_USED: &str = "x-format-used";
#[derive(Debug, Clone)]
pub struct LlmResponse<T> {
data: T,
format: OutputFormat,
include_token_headers: bool,
}
impl<T> LlmResponse<T> {
pub fn new(data: T, format: OutputFormat) -> Self {
Self {
data,
format,
include_token_headers: true,
}
}
pub fn json(data: T) -> Self {
Self::new(data, OutputFormat::Json)
}
pub fn toon(data: T) -> Self {
Self::new(data, OutputFormat::Toon)
}
pub fn without_token_headers(mut self) -> Self {
self.include_token_headers = false;
self
}
pub fn with_token_headers(mut self) -> Self {
self.include_token_headers = true;
self
}
}
fn estimate_tokens(text: &str) -> usize {
let char_count = text.len();
char_count.div_ceil(4) }
fn calculate_savings(json_tokens: usize, toon_tokens: usize) -> f64 {
if json_tokens == 0 {
return 0.0;
}
let savings = json_tokens.saturating_sub(toon_tokens) as f64 / json_tokens as f64 * 100.0;
(savings * 100.0).round() / 100.0 }
impl<T: Serialize> IntoResponse for LlmResponse<T> {
fn into_response(self) -> Response {
let json_result = serde_json::to_string(&self.data);
let toon_result = toon_format::encode_default(&self.data);
let (json_tokens, toon_tokens, savings) = if self.include_token_headers {
let json_tokens = json_result
.as_ref()
.map(|s| estimate_tokens(s))
.unwrap_or(0);
let toon_tokens = toon_result
.as_ref()
.map(|s| estimate_tokens(s))
.unwrap_or(0);
let savings = calculate_savings(json_tokens, toon_tokens);
(Some(json_tokens), Some(toon_tokens), Some(savings))
} else {
(None, None, None)
};
let (body, content_type) = match self.format {
OutputFormat::Json => match json_result {
Ok(json) => (json, JSON_CONTENT_TYPE),
Err(e) => {
tracing::error!("Failed to serialize to JSON: {}", e);
return ApiError::internal(format!("JSON serialization error: {}", e))
.into_response();
}
},
OutputFormat::Toon => match toon_result {
Ok(toon) => (toon, TOON_CONTENT_TYPE),
Err(e) => {
tracing::error!("Failed to serialize to TOON: {}", e);
return ApiError::internal(format!("TOON serialization error: {}", e))
.into_response();
}
},
};
let mut builder = http::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type)
.header(
X_FORMAT_USED,
match self.format {
OutputFormat::Json => "json",
OutputFormat::Toon => "toon",
},
);
if let Some(json_tokens) = json_tokens {
builder = builder.header(X_TOKEN_COUNT_JSON, json_tokens.to_string());
}
if let Some(toon_tokens) = toon_tokens {
builder = builder.header(X_TOKEN_COUNT_TOON, toon_tokens.to_string());
}
if let Some(savings) = savings {
builder = builder.header(X_TOKEN_SAVINGS, format!("{:.2}%", savings));
}
builder
.body(rustapi_core::ResponseBody::from(body))
.unwrap()
}
}
impl<T: Send> OperationModifier for LlmResponse<T> {
fn update_operation(_op: &mut Operation) {
}
}
impl<T: Serialize> ResponseModifier for LlmResponse<T> {
fn update_response(op: &mut Operation) {
let mut content = BTreeMap::new();
content.insert(
JSON_CONTENT_TYPE.to_string(),
MediaType {
schema: Some(SchemaRef::Inline(serde_json::json!({
"type": "object",
"description": "JSON formatted response with token counting headers"
}))),
example: None,
},
);
content.insert(
TOON_CONTENT_TYPE.to_string(),
MediaType {
schema: Some(SchemaRef::Inline(serde_json::json!({
"type": "string",
"description": "TOON (Token-Oriented Object Notation) formatted response with token counting headers"
}))),
example: None,
},
);
let response = ResponseSpec {
description: "LLM-optimized response with token counting headers (X-Token-Count-JSON, X-Token-Count-TOON, X-Token-Savings)".to_string(),
content,
headers: BTreeMap::new(),
};
op.responses.insert("200".to_string(), response);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
#[derive(Serialize)]
struct TestData {
id: u64,
name: String,
active: bool,
}
#[test]
fn test_estimate_tokens() {
assert_eq!(estimate_tokens(""), 0);
assert_eq!(estimate_tokens("test"), 1); assert_eq!(estimate_tokens("hello world"), 3); assert_eq!(estimate_tokens("a"), 1); }
#[test]
fn test_calculate_savings() {
assert_eq!(calculate_savings(100, 70), 30.0);
assert_eq!(calculate_savings(100, 80), 20.0);
assert_eq!(calculate_savings(100, 100), 0.0);
assert_eq!(calculate_savings(0, 0), 0.0);
}
#[test]
fn test_llm_response_json_format() {
let data = TestData {
id: 1,
name: "Test".to_string(),
active: true,
};
let response = LlmResponse::json(data);
assert!(matches!(response.format, OutputFormat::Json));
}
#[test]
fn test_llm_response_toon_format() {
let data = TestData {
id: 1,
name: "Test".to_string(),
active: true,
};
let response = LlmResponse::toon(data);
assert!(matches!(response.format, OutputFormat::Toon));
}
#[test]
fn test_llm_response_without_headers() {
let data = TestData {
id: 1,
name: "Test".to_string(),
active: true,
};
let response = LlmResponse::json(data).without_token_headers();
assert!(!response.include_token_headers);
}
#[test]
fn test_llm_response_with_headers() {
let data = TestData {
id: 1,
name: "Test".to_string(),
active: true,
};
let response = LlmResponse::toon(data)
.without_token_headers()
.with_token_headers();
assert!(response.include_token_headers);
}
}