use crate::server::AppState;
use axum::{
extract::{Query, State},
http::StatusCode,
response::Json,
};
use otelite_core::api::{
CostSeriesPoint, ErrorResponse, FinishReasonCount, TokenUsageResponse, TopSpan,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct TokenUsageQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/usage",
params(TokenUsageQuery),
responses(
(status = 200, description = "Token usage summary", body = TokenUsageResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_token_usage(
State(state): State<AppState>,
Query(query): Query<TokenUsageQuery>,
) -> Result<Json<TokenUsageResponse>, (StatusCode, Json<ErrorResponse>)> {
let (summary, by_model, by_system) = state
.storage
.query_token_usage(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query token usage: {}",
e
))),
)
})?;
Ok(Json(TokenUsageResponse {
summary,
by_model,
by_system,
}))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct CostSeriesQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub bucket: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/cost_series",
params(CostSeriesQuery),
responses(
(status = 200, description = "Cost series points", body = Vec<CostSeriesPoint>),
(status = 400, description = "Invalid bucket parameter", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_cost_series(
State(state): State<AppState>,
Query(query): Query<CostSeriesQuery>,
) -> Result<Json<Vec<CostSeriesPoint>>, (StatusCode, Json<ErrorResponse>)> {
let bucket_seconds = query.bucket.unwrap_or(3600);
if bucket_seconds <= 0 {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request(
"bucket must be a positive number of seconds",
)),
));
}
let bucket_ns = bucket_seconds.saturating_mul(1_000_000_000);
let series = state
.storage
.query_cost_series(query.start_time, query.end_time, bucket_ns)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query cost series: {}",
e
))),
)
})?;
Ok(Json(series))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct TopSpansQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub limit: Option<usize>,
}
#[utoipa::path(
get,
path = "/api/genai/top_spans",
params(TopSpansQuery),
responses(
(status = 200, description = "Top expensive spans", body = Vec<TopSpan>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_top_spans(
State(state): State<AppState>,
Query(query): Query<TopSpansQuery>,
) -> Result<Json<Vec<TopSpan>>, (StatusCode, Json<ErrorResponse>)> {
let limit = query.limit.unwrap_or(20).clamp(1, 100);
let spans = state
.storage
.query_top_spans(query.start_time, query.end_time, limit)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query top spans: {}",
e
))),
)
})?;
Ok(Json(spans))
}
#[derive(Debug, Deserialize, Serialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct FinishReasonsQuery {
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/genai/finish_reasons",
params(FinishReasonsQuery),
responses(
(status = 200, description = "Finish reason counts", body = Vec<FinishReasonCount>),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "genai"
)]
pub async fn get_finish_reasons(
State(state): State<AppState>,
Query(query): Query<FinishReasonsQuery>,
) -> Result<Json<Vec<FinishReasonCount>>, (StatusCode, Json<ErrorResponse>)> {
let rows = state
.storage
.query_finish_reasons(query.start_time, query.end_time)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::storage_error(format!(
"query finish reasons: {}",
e
))),
)
})?;
Ok(Json(rows))
}