mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-12 04:09:48 +08:00
Add provider key cycle stats reset handling
This commit is contained in:
@@ -2,6 +2,7 @@ mod batch;
|
||||
mod create;
|
||||
mod delete;
|
||||
mod oauth_invalid;
|
||||
mod reset_cycle_stats;
|
||||
mod update;
|
||||
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
@@ -30,6 +31,11 @@ pub(super) async fn maybe_handle(
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
if let Some(response) =
|
||||
reset_cycle_stats::maybe_handle(state, request_context, request_body).await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
if let Some(response) = create::maybe_handle(state, request_context, request_body).await? {
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
use crate::handlers::admin::provider::shared::paths::admin_reset_cycle_stats_key_id;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::provider_key_status_snapshot_payload;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
_request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if decision.route_family.as_deref() != Some("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("reset_cycle_stats")
|
||||
|| request_context.method() != http::Method::POST
|
||||
|| !request_context
|
||||
.path()
|
||||
.starts_with("/api/admin/endpoints/keys/")
|
||||
|| !request_context.path().ends_with("/reset-cycle-stats")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(key_id) = admin_reset_cycle_stats_key_id(request_context.path()) else {
|
||||
return Ok(Some(not_found_response("Key 不存在")));
|
||||
};
|
||||
let Some(mut key) = state
|
||||
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&key_id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
else {
|
||||
return Ok(Some(not_found_response(format!("Key {key_id} 不存在"))));
|
||||
};
|
||||
let Some(provider) = state
|
||||
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&key.provider_id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
else {
|
||||
return Ok(Some(not_found_response(format!(
|
||||
"Provider {} 不存在",
|
||||
key.provider_id
|
||||
))));
|
||||
};
|
||||
if !provider.provider_type.trim().eq_ignore_ascii_case("codex") {
|
||||
return Ok(Some(bad_request_response(
|
||||
"仅 Codex Provider 支持重置周期统计",
|
||||
)));
|
||||
}
|
||||
|
||||
let now_unix_secs = current_unix_secs();
|
||||
let mut status_snapshot = provider_key_status_snapshot_payload(&key, &provider.provider_type);
|
||||
let reset_windows = reset_codex_cycle_usage_windows(&mut status_snapshot, now_unix_secs);
|
||||
if reset_windows == 0 {
|
||||
return Ok(Some(bad_request_response("当前账号没有可重置的周期窗口")));
|
||||
}
|
||||
|
||||
key.status_snapshot = Some(status_snapshot);
|
||||
key.updated_at_unix_secs = Some(now_unix_secs);
|
||||
let Some(_) = state.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
Json(json!({
|
||||
"message": "已重置周期统计",
|
||||
"reset_at": now_unix_secs,
|
||||
"windows": reset_windows,
|
||||
}))
|
||||
.into_response(),
|
||||
))
|
||||
}
|
||||
|
||||
fn current_unix_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn reset_codex_cycle_usage_windows(status_snapshot: &mut Value, now_unix_secs: u64) -> usize {
|
||||
let Some(windows) = status_snapshot
|
||||
.get_mut("quota")
|
||||
.and_then(Value::as_object_mut)
|
||||
.and_then(|quota| quota.get_mut("windows"))
|
||||
.and_then(Value::as_array_mut)
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let mut reset_count = 0;
|
||||
for window in windows.iter_mut().filter_map(Value::as_object_mut) {
|
||||
let code = window
|
||||
.get("code")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
if !code.eq_ignore_ascii_case("5h") && !code.eq_ignore_ascii_case("weekly") {
|
||||
continue;
|
||||
}
|
||||
|
||||
window.insert("usage_reset_at".to_string(), json!(now_unix_secs));
|
||||
window.remove("usage");
|
||||
reset_count += 1;
|
||||
}
|
||||
|
||||
reset_count
|
||||
}
|
||||
|
||||
fn bad_request_response(detail: impl Into<String>) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail.into() })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn not_found_response(detail: impl Into<String>) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": detail.into() })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::reset_codex_cycle_usage_windows;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn reset_cycle_usage_marks_codex_windows_and_removes_stale_usage() {
|
||||
let mut snapshot = json!({
|
||||
"quota": {
|
||||
"windows": [
|
||||
{
|
||||
"code": "5h",
|
||||
"usage": {
|
||||
"request_count": 9,
|
||||
"total_tokens": 100,
|
||||
"total_cost_usd": "1.00000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "weekly",
|
||||
"usage": {
|
||||
"request_count": 10,
|
||||
"total_tokens": 200,
|
||||
"total_cost_usd": "2.00000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "monthly",
|
||||
"usage": {
|
||||
"request_count": 11
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(reset_codex_cycle_usage_windows(&mut snapshot, 1_234), 2);
|
||||
let windows = snapshot["quota"]["windows"].as_array().expect("windows");
|
||||
assert_eq!(windows[0]["usage_reset_at"], json!(1_234));
|
||||
assert!(windows[0].get("usage").is_none());
|
||||
assert_eq!(windows[1]["usage_reset_at"], json!(1_234));
|
||||
assert!(windows[1].get("usage").is_none());
|
||||
assert!(windows[2].get("usage_reset_at").is_none());
|
||||
assert!(windows[2].get("usage").is_some());
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::super::oauth::quota::antigravity::refresh_antigravity_provider_quota_locally;
|
||||
use super::super::oauth::quota::chatgpt_web::refresh_chatgpt_web_provider_quota_locally;
|
||||
@@ -138,20 +138,28 @@ pub(super) async fn maybe_handle(
|
||||
));
|
||||
};
|
||||
|
||||
let mut keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
keys = if let Some(selected_key_ids) = selected_key_ids.as_ref() {
|
||||
let keys = if let Some(selected_key_ids) = selected_key_ids.as_ref() {
|
||||
if selected_key_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let selected = selected_key_ids.iter().cloned().collect::<BTreeSet<_>>();
|
||||
keys.into_iter()
|
||||
.filter(|key| selected.contains(&key.id))
|
||||
let mut by_id = state
|
||||
.read_provider_catalog_keys_by_ids(selected_key_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| key.provider_id == provider_id && selected.contains(&key.id))
|
||||
.map(|key| (key.id.clone(), key))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
selected_key_ids
|
||||
.iter()
|
||||
.filter_map(|key_id| by_id.remove(key_id))
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
keys.into_iter()
|
||||
state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| {
|
||||
key.is_active
|
||||
|| key
|
||||
|
||||
Reference in New Issue
Block a user