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:
RWDai
2026-04-17 14:21:43 +08:00
committed by GitHub
parent e5d3722adf
commit b8702ae124
39 changed files with 1739 additions and 128 deletions

View File

@@ -106,7 +106,7 @@
钱包
</TableHead>
<TableHead class="w-[190px] h-12 font-semibold">
统计/
统计/
</TableHead>
<TableHead class="w-[110px] h-12 font-semibold">
有效期
@@ -239,6 +239,22 @@
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
</span>
</div>
<div class="flex items-center gap-1 text-muted-foreground">
<span>并发:</span>
<Badge
v-if="isConcurrentLimitInherited(apiKey.concurrent_limit) || isConcurrentLimitUnlimited(apiKey.concurrent_limit)"
variant="secondary"
class="h-5 px-1.5 py-0 text-[10px] font-medium"
>
{{ formatConcurrentLimitInheritable(apiKey.concurrent_limit) }}
</Badge>
<span
v-else
class="font-medium text-foreground"
>
{{ formatConcurrentLimitInheritable(apiKey.concurrent_limit) }}
</span>
</div>
</div>
</TableCell>
<TableCell class="py-4">
@@ -409,6 +425,12 @@
>
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
</Badge>
<Badge
variant="secondary"
class="h-5 px-1.5 py-0 text-[10px] font-medium"
>
{{ formatConcurrentLimitInheritable(apiKey.concurrent_limit) }}
</Badge>
<Badge
v-if="apiKey.auto_delete_on_expiry"
variant="secondary"
@@ -920,6 +942,7 @@ function editApiKey(apiKey: AdminApiKey) {
unlimited_balance: isApiKeyUnlimited(apiKey),
expires_at: expiresAt,
rate_limit: apiKey.rate_limit ?? undefined,
concurrent_limit: apiKey.concurrent_limit ?? undefined,
auto_delete_on_expiry: apiKey.auto_delete_on_expiry || false,
allowed_providers: apiKey.allowed_providers == null ? null : [...apiKey.allowed_providers],
allowed_api_formats: apiKey.allowed_api_formats == null ? null : [...apiKey.allowed_api_formats],
@@ -961,6 +984,20 @@ function formatApiKeyTotalTokens(apiKey: AdminApiKey): string {
return formatTokens(apiKey.total_tokens)
}
function formatConcurrentLimitInheritable(concurrentLimit?: number | null): string {
if (concurrentLimit == null) return '不限并发'
if (concurrentLimit === 0) return '不限并发'
return `${concurrentLimit} 并发`
}
function isConcurrentLimitInherited(concurrentLimit?: number | null): boolean {
return concurrentLimit == null
}
function isConcurrentLimitUnlimited(concurrentLimit?: number | null): boolean {
return concurrentLimit === 0
}
function formatWalletAmount(value: number | null, nullLabel = '无限制'): string {
if (value == null) {
return nullLabel
@@ -1110,6 +1147,7 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
name: data.name || undefined,
unlimited_balance: Boolean(data.unlimited_balance),
rate_limit: data.rate_limit ?? null, // undefined = 跟随系统默认,显式传 null
concurrent_limit: data.concurrent_limit ?? null,
expires_at: serializeExpiryDate(data.expires_at),
auto_delete_on_expiry: data.auto_delete_on_expiry,
// 空数组表示清除限制(允许全部),后端会将空数组存为 NULL
@@ -1139,6 +1177,7 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
name: data.name || undefined,
initial_balance_usd: isUnlimited ? null : (data.initial_balance_usd as number),
rate_limit: data.rate_limit ?? null, // undefined = 跟随系统默认,显式传 null
concurrent_limit: data.concurrent_limit ?? null,
expires_at: serializeExpiryDate(data.expires_at),
auto_delete_on_expiry: data.auto_delete_on_expiry,
// 空数组表示不设置限制(允许全部),后端会将空数组存为 NULL

View File

@@ -679,6 +679,12 @@
>
{{ formatRateLimitSimple(apiKey.rate_limit) }}
</Badge>
<Badge
variant="secondary"
class="text-xs"
>
{{ formatConcurrentLimitSimple(apiKey.concurrent_limit) }}
</Badge>
</div>
<div class="flex items-center gap-1 mt-0.5">
<code class="text-xs font-mono text-muted-foreground">
@@ -795,7 +801,7 @@
{{ editingUserApiKey ? '编辑 API Key' : '创建 API Key' }}
</h3>
<p class="text-xs text-muted-foreground">
{{ editingUserApiKey ? '更新用户 API Key 的名称速率限制' : '为用户创建新的 API Key' }}
{{ editingUserApiKey ? '更新用户 API Key 的名称速率限制和并发限制' : '为用户创建新的 API Key' }}
</p>
</div>
</div>
@@ -834,6 +840,25 @@
留空表示不限制
</p>
</div>
<div class="space-y-2">
<Label
for="admin-user-key-concurrent-limit"
class="text-sm font-medium"
>并发限制</Label>
<Input
id="admin-user-key-concurrent-limit"
:model-value="userApiKeyForm.concurrent_limit ?? ''"
type="number"
min="0"
max="10000"
class="h-10"
placeholder="0 = 不限并发"
@update:model-value="(v) => userApiKeyForm.concurrent_limit = parseNumberInput(v, { min: 0, max: 10000 })"
/>
<p class="text-xs text-muted-foreground">
{{ editingUserApiKey ? '留空表示保持当前值,填 0 表示不限并发' : '留空表示不限并发,填 0 也表示不限并发' }}
</p>
</div>
</div>
<template #footer>
@@ -1099,6 +1124,7 @@ const editingUserApiKey = ref<ApiKey | null>(null)
const userApiKeyForm = ref({
name: '',
rate_limit: undefined as number | undefined,
concurrent_limit: undefined as number | undefined,
})
// 用户统计
@@ -1253,6 +1279,13 @@ function formatCurrencyValue(value: number | null, nullLabel = '-'): string {
return `$${value.toFixed(2)}`
}
function formatConcurrentLimitSimple(concurrentLimit?: number | null): string {
if (concurrentLimit == null || concurrentLimit === 0) {
return '不限并发'
}
return `${concurrentLimit} 并发`
}
function isNegativeWalletValue(value: number | null): boolean {
return typeof value === 'number' && value < 0
}
@@ -1387,6 +1420,7 @@ function openCreateUserApiKeyDialog() {
userApiKeyForm.value = {
name: `Key-${new Date().toISOString().split('T')[0]}`,
rate_limit: undefined,
concurrent_limit: undefined,
}
editingUserApiKey.value = null
showUserApiKeyFormDialog.value = true
@@ -1397,6 +1431,7 @@ function openEditUserApiKeyDialog(apiKey: ApiKey) {
userApiKeyForm.value = {
name: apiKey.name || '',
rate_limit: apiKey.rate_limit ?? undefined,
concurrent_limit: apiKey.concurrent_limit ?? undefined,
}
showUserApiKeyFormDialog.value = true
}
@@ -1407,6 +1442,7 @@ function closeUserApiKeyFormDialog() {
userApiKeyForm.value = {
name: '',
rate_limit: undefined,
concurrent_limit: undefined,
}
}
@@ -1423,12 +1459,14 @@ async function submitUserApiKeyForm() {
await usersStore.updateApiKey(selectedUser.value.id, editingUserApiKey.value.id, {
name: userApiKeyForm.value.name,
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
concurrent_limit: userApiKeyForm.value.concurrent_limit,
})
success('API Key已更新')
} else {
const response = await usersStore.createApiKey(selectedUser.value.id, {
name: userApiKeyForm.value.name,
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
concurrent_limit: userApiKeyForm.value.concurrent_limit,
})
newApiKey.value = response.key || ''
showNewApiKeyDialog.value = true

View File

@@ -174,6 +174,12 @@
>
{{ formatRateLimitSimple(apiKey.rate_limit) }}
</Badge>
<Badge
variant="secondary"
class="h-5 px-2 py-0 text-[10px] font-medium"
>
{{ formatConcurrentLimitSimple(apiKey.concurrent_limit) }}
</Badge>
</div>
</TableCell>
@@ -259,6 +265,12 @@
>
{{ formatRateLimitSimple(apiKey.rate_limit) }}
</Badge>
<Badge
variant="secondary"
class="text-[10px] px-1.5 py-0"
>
{{ formatConcurrentLimitSimple(apiKey.concurrent_limit) }}
</Badge>
</div>
<div class="flex items-center gap-0.5 flex-shrink-0">
<Button
@@ -355,7 +367,7 @@
{{ editingApiKey ? '编辑 API 密钥' : '创建 API 密钥' }}
</h3>
<p class="text-xs text-muted-foreground">
{{ editingApiKey ? '更新密钥名称速率限制' : '创建一个新的密钥用于访问 API 服务' }}
{{ editingApiKey ? '更新密钥名称速率限制和并发限制' : '创建一个新的密钥用于访问 API 服务' }}
</p>
</div>
</div>
@@ -400,6 +412,26 @@
留空不限
</p>
</div>
<div class="space-y-2">
<Label
for="key-concurrent-limit"
class="text-sm font-semibold"
>并发限制</Label>
<Input
id="key-concurrent-limit"
:model-value="newKeyConcurrentLimit ?? ''"
type="number"
min="0"
max="10000"
placeholder="0 = 不限并发"
class="h-11 border-border/60"
@update:model-value="(v) => newKeyConcurrentLimit = parseNumberInput(v, { min: 0, max: 10000 })"
/>
<p class="text-xs text-muted-foreground">
{{ editingApiKey ? '留空表示保持当前值,填 0 表示不限并发' : '留空表示不限并发,填 0 也表示不限并发' }}
</p>
</div>
</div>
<template #footer>
@@ -542,6 +574,7 @@ const showDeleteDialog = ref(false)
const newKeyName = ref('')
const newKeyRateLimit = ref<number | undefined>(undefined)
const newKeyConcurrentLimit = ref<number | undefined>(undefined)
const newKeyValue = ref('')
const keyToDelete = ref<ApiKey | null>(null)
const editingApiKey = ref<ApiKey | null>(null)
@@ -573,6 +606,7 @@ function openEditApiKeyDialog(apiKey: ApiKey) {
editingApiKey.value = apiKey
newKeyName.value = apiKey.name || ''
newKeyRateLimit.value = apiKey.rate_limit ?? undefined
newKeyConcurrentLimit.value = apiKey.concurrent_limit ?? undefined
showCreateDialog.value = true
}
@@ -580,6 +614,7 @@ function openCreateApiKeyDialog() {
editingApiKey.value = null
newKeyName.value = ''
newKeyRateLimit.value = undefined
newKeyConcurrentLimit.value = undefined
showCreateDialog.value = true
}
@@ -588,6 +623,7 @@ function closeApiKeyDialog() {
editingApiKey.value = null
newKeyName.value = ''
newKeyRateLimit.value = undefined
newKeyConcurrentLimit.value = undefined
}
async function saveApiKey() {
@@ -602,12 +638,14 @@ async function saveApiKey() {
await meApi.updateApiKey(editingApiKey.value.id, {
name: newKeyName.value,
rate_limit: newKeyRateLimit.value ?? 0,
concurrent_limit: newKeyConcurrentLimit.value,
})
success('API 密钥更新成功')
} else {
const newKey = await meApi.createApiKey({
name: newKeyName.value,
rate_limit: newKeyRateLimit.value ?? 0,
concurrent_limit: newKeyConcurrentLimit.value,
})
newKeyValue.value = newKey.key || ''
showKeyDialog.value = true
@@ -711,6 +749,13 @@ function formatNumber(num: number | undefined | null): string {
return num.toLocaleString('zh-CN')
}
function formatConcurrentLimitSimple(concurrentLimit?: number | null): string {
if (concurrentLimit == null || concurrentLimit === 0) {
return '不限并发'
}
return `${concurrentLimit} 并发`
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {