Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/routes/admin_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,13 @@ pub struct ModelPatch {
}

/// 校验通用字段:价格 ≥ 0、vision ∈ {0,1}、currency 枚举
#[allow(clippy::too_many_arguments)] // 价格字段平铺(6 个价格 + currency + vision),保持与请求结构体同序
fn validate_common(
currency: &str,
input_per_m: f64,
output_per_m: f64,
vision: i64,
cache_hit_input_per_m: f64,
peak_input_per_m: f64,
peak_output_per_m: f64,
peak_cache_hit_input_per_m: f64,
Expand All @@ -86,6 +88,7 @@ fn validate_common(
}
if input_per_m < 0.0
|| output_per_m < 0.0
|| cache_hit_input_per_m < 0.0
|| peak_input_per_m < 0.0
|| peak_output_per_m < 0.0
|| peak_cache_hit_input_per_m < 0.0
Expand Down Expand Up @@ -162,6 +165,7 @@ pub async fn create(
req.input_per_m,
req.output_per_m,
req.vision,
req.cache_hit_input_per_m,
req.peak_input_per_m,
req.peak_output_per_m,
req.peak_cache_hit_input_per_m,
Expand Down Expand Up @@ -297,6 +301,7 @@ pub async fn patch(
input_per_m,
output_per_m,
vision,
cache_hit_input_per_m,
peak_input_per_m,
peak_output_per_m,
peak_cache_hit_input_per_m,
Expand Down
66 changes: 66 additions & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,72 @@ mod tests {
assert_eq!(s, StatusCode::NOT_FOUND, "重复删除应 404");
}

#[tokio::test]
async fn admin_models_rejects_negative_cache_hit_price() {
// 负数缓存命中价会一路无钳制进入结算(pts 变负 ⇒ 消费者被充值、分享者被倒扣),
// 故必须与其余 5 个价格字段一样在入口被拒。修复前 CREATE/PATCH 均返回 200 并落库。
let st = test_state("admnegcache");
let admin_bearer = login_bearer(&st, "admin@aitokenpool.local", "admin1234").await;

// CREATE:负数 cache_hit_input_per_m → 400
let (s, body) = post(
st.clone(),
"/api/admin/models",
r#"{"provider":"neg","model":"neg-cache","currency":"USD","input_per_m":1.0,"output_per_m":2.0,"cache_hit_input_per_m":-3.0}"#,
Some(&admin_bearer),
)
.await;
assert_eq!(s, StatusCode::BAD_REQUEST, "负数缓存命中价应 400: {body}");

// 阳性对照:0 是合法值(= 命中部分免费),必须放行
let (s, body) = post(
st.clone(),
"/api/admin/models",
r#"{"provider":"neg","model":"zero-cache","currency":"USD","input_per_m":1.0,"output_per_m":2.0,"cache_hit_input_per_m":0.0}"#,
Some(&admin_bearer),
)
.await;
assert_eq!(s, StatusCode::OK, "0 缓存命中价应 200: {body}");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let new_id = v["id"].as_i64().unwrap();

// PATCH:改成负数 → 400,且库中该值不被写入
let (s, body) = patch(
st.clone(),
&format!("/api/admin/models/{new_id}"),
r#"{"cache_hit_input_per_m":-9.0}"#,
Some(&admin_bearer),
)
.await;
assert_eq!(
s,
StatusCode::BAD_REQUEST,
"PATCH 负数缓存命中价应 400: {body}"
);
let (_, body) = get(st.clone(), "/api/admin/models", Some(&admin_bearer)).await;
let arr: serde_json::Value = serde_json::from_str(&body).unwrap();
let row = arr
.as_array()
.unwrap()
.iter()
.find(|m| m["id"] == new_id)
.expect("模型仍在");
assert_eq!(
row["cache_hit_input_per_m"], 0.0,
"被拒的 PATCH 不得改库: {body}"
);

// 兄弟字段(高峰变体)此前已被校验,须保持 400
let (s, _) = post(
st.clone(),
"/api/admin/models",
r#"{"provider":"neg","model":"neg-peak","currency":"USD","input_per_m":1.0,"peak_cache_hit_input_per_m":-3.0}"#,
Some(&admin_bearer),
)
.await;
assert_eq!(s, StatusCode::BAD_REQUEST, "高峰缓存命中价负数仍应 400");
}

#[tokio::test]
async fn market_models_include_new_fields() {
// GET /api/models 响应补 context_length/max_output/vision/cache_hit_input_per_m
Expand Down
Loading