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 <[email protected]>
This commit is contained in:
RWDai
2026-04-17 14:21:43 +08:00
committed by GitHub
co-authored by fawney19
parent e5d3722adf
commit b8702ae124
39 changed files with 1739 additions and 128 deletions
@@ -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)]