mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
Fix/api key concurrency runtime miss (#309)
* test(cli): 覆盖 API key 并发等待与超时路径 * feat(scheduler): API key 并发饱和时等待可用槽位 * fix(proxy): 区分 API key 并发受限与真正的 runtime miss * fix(outcome): runtime miss 仅归因真实执行候选 * feat(api-keys): 统一 concurrent_limit 默认值与校验辅助 * feat(admin): 独立 Key 接口支持 concurrent_limit * feat(admin): 用户 API Key 路由支持 concurrent_limit * feat(public): 自助 API Key 路由支持 concurrent_limit * feat(import): 导入与存储层持久化 concurrent_limit * feat(frontend): 同步 API Key concurrent_limit 类型定义 * feat(frontend): 独立 Key 表单支持 concurrent_limit * feat(frontend): 管理员用户 API Key 表单支持 concurrent_limit * feat(frontend): 自助 API Key 页面支持 concurrent_limit * chore(fmt): 统一 runtime 归因相关 Rust 格式 * chore(fmt): 统一 admin API key 路由 Rust 格式 * chore(fmt): 统一 public 路由与相关测试 Rust 格式 * fix(test): 对齐 no-execution usage 归因断言 * test(middleware): 固定 access log tracing 用例线程模型 * fix(frontend): 提取用户 API Key payload 默认并发辅助 * fix(frontend): 保留用户 Key 的 concurrent_limit 默认值 * fix(api-keys): remove hardcoded concurrent limit default --------- Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ use crate::handlers::admin::users::{
|
||||
normalize_admin_optional_api_key_name, normalize_admin_user_api_formats,
|
||||
normalize_admin_user_string_list,
|
||||
};
|
||||
use crate::handlers::shared::normalize_optional_api_key_concurrent_limit;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::serialize_admin_system_users_export_wallet;
|
||||
use axum::{
|
||||
@@ -114,6 +115,11 @@ pub(super) async fn build_admin_create_api_key_response(
|
||||
"rate_limit 必须大于等于 0",
|
||||
));
|
||||
}
|
||||
let concurrent_limit =
|
||||
match normalize_optional_api_key_concurrent_limit(payload.concurrent_limit) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => return Ok(build_admin_api_keys_bad_request_response(detail)),
|
||||
};
|
||||
let (initial_balance_usd, unlimited_balance) = match normalize_standalone_initial_balance(
|
||||
payload.initial_balance_usd,
|
||||
payload.unlimited_balance,
|
||||
@@ -154,7 +160,7 @@ pub(super) async fn build_admin_create_api_key_response(
|
||||
allowed_api_formats,
|
||||
allowed_models,
|
||||
rate_limit: payload.rate_limit,
|
||||
concurrent_limit: 5,
|
||||
concurrent_limit,
|
||||
force_capabilities: None,
|
||||
is_active: true,
|
||||
expires_at_unix_secs,
|
||||
@@ -184,6 +190,7 @@ pub(super) async fn build_admin_create_api_key_response(
|
||||
"is_standalone": true,
|
||||
"is_active": created.is_active,
|
||||
"rate_limit": created.rate_limit,
|
||||
"concurrent_limit": created.concurrent_limit,
|
||||
"allowed_providers": created.allowed_providers,
|
||||
"allowed_api_formats": created.allowed_api_formats,
|
||||
"allowed_models": created.allowed_models,
|
||||
@@ -270,6 +277,11 @@ pub(super) async fn build_admin_update_api_key_response(
|
||||
"rate_limit 必须大于等于 0",
|
||||
));
|
||||
}
|
||||
let concurrent_limit =
|
||||
match normalize_optional_api_key_concurrent_limit(payload.concurrent_limit) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => return Ok(build_admin_api_keys_bad_request_response(detail)),
|
||||
};
|
||||
let allowed_providers = if field_presence.contains("allowed_providers") {
|
||||
match normalize_admin_user_string_list(payload.allowed_providers, "allowed_providers") {
|
||||
Ok(value) => Some(value),
|
||||
@@ -356,6 +368,8 @@ pub(super) async fn build_admin_update_api_key_response(
|
||||
name,
|
||||
rate_limit_present: field_presence.contains("rate_limit"),
|
||||
rate_limit: payload.rate_limit,
|
||||
concurrent_limit_present: field_presence.contains("concurrent_limit"),
|
||||
concurrent_limit,
|
||||
allowed_providers,
|
||||
allowed_api_formats,
|
||||
allowed_models,
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(super) struct AdminStandaloneApiKeyCreateRequest {
|
||||
pub(super) allowed_api_formats: Option<Vec<String>>,
|
||||
pub(super) allowed_models: Option<Vec<String>>,
|
||||
pub(super) rate_limit: Option<i32>,
|
||||
pub(super) concurrent_limit: Option<i32>,
|
||||
pub(super) initial_balance_usd: Option<f64>,
|
||||
pub(super) unlimited_balance: Option<bool>,
|
||||
pub(super) expire_days: Option<i32>,
|
||||
@@ -36,6 +37,7 @@ pub(super) struct AdminStandaloneApiKeyUpdateRequest {
|
||||
pub(super) allowed_api_formats: Option<Vec<String>>,
|
||||
pub(super) allowed_models: Option<Vec<String>>,
|
||||
pub(super) rate_limit: Option<i32>,
|
||||
pub(super) concurrent_limit: Option<i32>,
|
||||
pub(super) initial_balance_usd: Option<f64>,
|
||||
pub(super) unlimited_balance: Option<bool>,
|
||||
pub(super) expire_days: Option<i32>,
|
||||
@@ -144,6 +146,7 @@ pub(super) fn build_admin_api_key_list_item_payload(
|
||||
"total_tokens": total_tokens,
|
||||
"total_cost_usd": record.total_cost_usd,
|
||||
"rate_limit": record.rate_limit,
|
||||
"concurrent_limit": record.concurrent_limit,
|
||||
"allowed_providers": record.allowed_providers,
|
||||
"allowed_api_formats": record.allowed_api_formats,
|
||||
"allowed_models": record.allowed_models,
|
||||
@@ -173,6 +176,7 @@ pub(super) fn build_admin_api_key_detail_payload(
|
||||
"total_tokens": total_tokens,
|
||||
"total_cost_usd": record.total_cost_usd,
|
||||
"rate_limit": record.rate_limit,
|
||||
"concurrent_limit": record.concurrent_limit,
|
||||
"allowed_providers": record.allowed_providers,
|
||||
"allowed_api_formats": record.allowed_api_formats,
|
||||
"allowed_models": record.allowed_models,
|
||||
|
||||
@@ -1871,8 +1871,10 @@ impl<'a> AdminAppState<'a> {
|
||||
let concurrent_limit = invalid_value!(imported_optional_i32(
|
||||
key.get("concurrent_limit"),
|
||||
"concurrent_limit"
|
||||
))
|
||||
.unwrap_or(5);
|
||||
));
|
||||
if concurrent_limit.is_some_and(|value| value < 0) {
|
||||
return Ok(Err(invalid_request("concurrent_limit 必须是非负整数")));
|
||||
}
|
||||
let force_capabilities = imported_optional_value(key.get("force_capabilities"));
|
||||
let is_active =
|
||||
invalid_value!(imported_optional_bool(key.get("is_active"))).unwrap_or(true);
|
||||
@@ -1913,6 +1915,11 @@ impl<'a> AdminAppState<'a> {
|
||||
api_key_id: existing_key.api_key_id.clone(),
|
||||
name: name.clone(),
|
||||
rate_limit: Some(rate_limit),
|
||||
concurrent_limit: if key.contains_key("concurrent_limit") {
|
||||
concurrent_limit
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -2051,8 +2058,10 @@ impl<'a> AdminAppState<'a> {
|
||||
let concurrent_limit = invalid_value!(imported_optional_i32(
|
||||
key.get("concurrent_limit"),
|
||||
"concurrent_limit"
|
||||
))
|
||||
.unwrap_or(5);
|
||||
));
|
||||
if concurrent_limit.is_some_and(|value| value < 0) {
|
||||
return Ok(Err(invalid_request("concurrent_limit 必须是非负整数")));
|
||||
}
|
||||
let force_capabilities = imported_optional_value(key.get("force_capabilities"));
|
||||
let is_active =
|
||||
invalid_value!(imported_optional_bool(key.get("is_active"))).unwrap_or(true);
|
||||
@@ -2099,6 +2108,8 @@ impl<'a> AdminAppState<'a> {
|
||||
name: name.clone(),
|
||||
rate_limit_present: true,
|
||||
rate_limit: Some(rate_limit),
|
||||
concurrent_limit_present: key.contains_key("concurrent_limit"),
|
||||
concurrent_limit,
|
||||
allowed_providers: Some(allowed_providers.clone()),
|
||||
allowed_api_formats: Some(allowed_api_formats.clone()),
|
||||
allowed_models: Some(allowed_models.clone()),
|
||||
@@ -2123,7 +2134,6 @@ impl<'a> AdminAppState<'a> {
|
||||
|| key.contains_key("force_capabilities")
|
||||
|| key.contains_key("total_requests")
|
||||
|| key.contains_key("total_cost_usd")
|
||||
|| key.contains_key("concurrent_limit")
|
||||
{
|
||||
stats.errors.push(
|
||||
"现有独立余额 Key 仅覆盖基础字段;高级导入字段保持原值".to_string(),
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(super) fn build_admin_user_api_key_detail_payload(
|
||||
"total_requests": record.total_requests,
|
||||
"total_cost_usd": record.total_cost_usd,
|
||||
"rate_limit": record.rate_limit,
|
||||
"concurrent_limit": record.concurrent_limit,
|
||||
"expires_at": format_optional_unix_secs_iso8601(record.expires_at_unix_secs),
|
||||
"last_used_at": serde_json::Value::Null,
|
||||
"created_at": serde_json::Value::Null,
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::super::helpers::{
|
||||
use super::super::paths::admin_user_id_from_api_keys_path;
|
||||
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::shared::normalize_optional_api_key_concurrent_limit;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -69,7 +70,7 @@ pub(crate) async fn build_admin_create_user_api_key_response(
|
||||
{
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "当前仅支持 name、rate_limit、allowed_providers 字段" })),
|
||||
Json(json!({ "detail": "当前仅支持 name、rate_limit、concurrent_limit、allowed_providers 字段" })),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
@@ -103,6 +104,17 @@ pub(crate) async fn build_admin_create_user_api_key_response(
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
let concurrent_limit =
|
||||
match normalize_optional_api_key_concurrent_limit(payload.concurrent_limit) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => {
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let plaintext_key = generate_admin_user_api_key_plaintext();
|
||||
let Some(key_encrypted) = state.encrypt_catalog_secret_with_fallbacks(&plaintext_key) else {
|
||||
@@ -124,7 +136,7 @@ pub(crate) async fn build_admin_create_user_api_key_response(
|
||||
allowed_api_formats: None,
|
||||
allowed_models: None,
|
||||
rate_limit,
|
||||
concurrent_limit: 5,
|
||||
concurrent_limit,
|
||||
force_capabilities: None,
|
||||
is_active: true,
|
||||
expires_at_unix_secs: None,
|
||||
@@ -156,6 +168,7 @@ pub(crate) async fn build_admin_create_user_api_key_response(
|
||||
"name": created.name,
|
||||
"key_display": masked_user_api_key_display(state, created.key_encrypted.as_deref()),
|
||||
"rate_limit": created.rate_limit,
|
||||
"concurrent_limit": created.concurrent_limit,
|
||||
"expires_at": format_optional_unix_secs_iso8601(created.expires_at_unix_secs),
|
||||
"created_at": chrono::Utc::now().to_rfc3339(),
|
||||
"message": "API Key创建成功,请妥善保存完整密钥",
|
||||
|
||||
@@ -62,6 +62,7 @@ pub(crate) async fn build_admin_list_user_api_keys_response(
|
||||
"total_requests": record.total_requests,
|
||||
"total_cost_usd": record.total_cost_usd,
|
||||
"rate_limit": record.rate_limit,
|
||||
"concurrent_limit": record.concurrent_limit,
|
||||
"expires_at": format_optional_unix_secs_iso8601(record.expires_at_unix_secs),
|
||||
"last_used_at": serde_json::Value::Null,
|
||||
"created_at": serde_json::Value::Null,
|
||||
|
||||
@@ -9,6 +9,7 @@ use super::super::helpers::{
|
||||
use super::super::paths::admin_user_api_key_parts;
|
||||
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::shared::normalize_optional_api_key_concurrent_limit;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -68,6 +69,17 @@ pub(crate) async fn build_admin_update_user_api_key_response(
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
let concurrent_limit =
|
||||
match normalize_optional_api_key_concurrent_limit(payload.concurrent_limit) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => {
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let Some(updated) = state
|
||||
.update_user_api_key_basic(aether_data::repository::auth::UpdateUserApiKeyBasicRecord {
|
||||
@@ -75,6 +87,7 @@ pub(crate) async fn build_admin_update_user_api_key_response(
|
||||
api_key_id: api_key_id.clone(),
|
||||
name,
|
||||
rate_limit: payload.rate_limit,
|
||||
concurrent_limit,
|
||||
})
|
||||
.await?
|
||||
else {
|
||||
|
||||
@@ -22,6 +22,8 @@ pub(super) struct AdminCreateUserApiKeyRequest {
|
||||
#[serde(default)]
|
||||
pub(super) rate_limit: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub(super) concurrent_limit: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub(super) expire_days: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub(super) expires_at: Option<String>,
|
||||
@@ -41,6 +43,8 @@ pub(super) struct AdminUpdateUserApiKeyRequest {
|
||||
pub(super) name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) rate_limit: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub(super) concurrent_limit: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user