mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +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:
@@ -203,6 +203,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="form-concurrent-limit"
|
||||
class="text-sm font-medium"
|
||||
>并发限制</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Input
|
||||
v-if="!form.concurrent_limit_inherited"
|
||||
id="form-concurrent-limit"
|
||||
:model-value="form.concurrent_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
placeholder="0 = 不限制"
|
||||
class="h-10"
|
||||
@update:model-value="(v) => form.concurrent_limit = parseNumberInput(v, { min: 0, max: 10000 })"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="flex h-10 w-full items-center rounded-lg border bg-background px-3 text-sm text-muted-foreground opacity-60"
|
||||
>不限制</span>
|
||||
</div>
|
||||
<Switch
|
||||
v-model="form.concurrent_limit_inherited"
|
||||
class="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空表示不限制,填 0 也表示不限制并发
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 额度 -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">额度</Label>
|
||||
@@ -288,6 +321,7 @@ export interface StandaloneKeyFormData {
|
||||
unlimited_balance?: boolean
|
||||
expires_at?: string // ISO 日期字符串,如 "2025-12-31",undefined = 永不过期
|
||||
rate_limit?: number | null
|
||||
concurrent_limit?: number | null
|
||||
auto_delete_on_expiry: boolean
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
@@ -303,6 +337,8 @@ interface StandaloneKeyFormState {
|
||||
expires_at?: string
|
||||
rate_limit_inherited: boolean
|
||||
rate_limit?: number
|
||||
concurrent_limit_inherited: boolean
|
||||
concurrent_limit?: number
|
||||
auto_delete_on_expiry: boolean
|
||||
provider_unrestricted: boolean
|
||||
api_format_unrestricted: boolean
|
||||
@@ -358,6 +394,8 @@ const form = ref<StandaloneKeyFormState>({
|
||||
expires_at: undefined,
|
||||
rate_limit_inherited: true,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit_inherited: true,
|
||||
concurrent_limit: undefined,
|
||||
auto_delete_on_expiry: false,
|
||||
provider_unrestricted: true,
|
||||
api_format_unrestricted: true,
|
||||
@@ -402,6 +440,8 @@ function resetForm() {
|
||||
expires_at: undefined,
|
||||
rate_limit_inherited: true,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit_inherited: true,
|
||||
concurrent_limit: undefined,
|
||||
auto_delete_on_expiry: false,
|
||||
provider_unrestricted: true,
|
||||
api_format_unrestricted: true,
|
||||
@@ -423,6 +463,8 @@ function loadKeyData() {
|
||||
expires_at: props.apiKey.expires_at,
|
||||
rate_limit_inherited: props.apiKey.rate_limit == null,
|
||||
rate_limit: props.apiKey.rate_limit ?? undefined,
|
||||
concurrent_limit_inherited: props.apiKey.concurrent_limit == null,
|
||||
concurrent_limit: props.apiKey.concurrent_limit ?? undefined,
|
||||
auto_delete_on_expiry: props.apiKey.auto_delete_on_expiry,
|
||||
provider_unrestricted: props.apiKey.allowed_providers == null,
|
||||
api_format_unrestricted: props.apiKey.allowed_api_formats == null,
|
||||
@@ -473,6 +515,7 @@ function handleSubmit() {
|
||||
unlimited_balance: form.value.unlimited_balance,
|
||||
expires_at: form.value.expires_at,
|
||||
rate_limit: form.value.rate_limit_inherited ? null : (form.value.rate_limit ?? 0),
|
||||
concurrent_limit: form.value.concurrent_limit_inherited ? null : (form.value.concurrent_limit ?? 0),
|
||||
auto_delete_on_expiry: form.value.auto_delete_on_expiry,
|
||||
allowed_providers: form.value.provider_unrestricted ? null : [...form.value.allowed_providers],
|
||||
allowed_api_formats: form.value.api_format_unrestricted ? null : [...form.value.allowed_api_formats],
|
||||
@@ -503,6 +546,15 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.value.concurrent_limit_inherited,
|
||||
(inherited) => {
|
||||
if (!inherited && form.value.concurrent_limit == null) {
|
||||
form.value.concurrent_limit = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
setSaving
|
||||
})
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildUserApiKeyMutationPayload } from '@/features/api-keys/utils/userKeyPayload'
|
||||
|
||||
describe('userKeyPayload', () => {
|
||||
it('omits concurrent_limit when the field is left blank', () => {
|
||||
expect(buildUserApiKeyMutationPayload({
|
||||
name: 'writer-key',
|
||||
rate_limit: 30,
|
||||
concurrent_limit: undefined,
|
||||
})).toEqual({
|
||||
name: 'writer-key',
|
||||
rate_limit: 30,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps explicit unlimited concurrent_limit values', () => {
|
||||
expect(buildUserApiKeyMutationPayload({
|
||||
name: 'writer-key',
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: 0,
|
||||
})).toEqual({
|
||||
name: 'writer-key',
|
||||
rate_limit: 0,
|
||||
concurrent_limit: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps positive concurrent_limit values', () => {
|
||||
expect(buildUserApiKeyMutationPayload({
|
||||
name: 'writer-key',
|
||||
rate_limit: 15,
|
||||
concurrent_limit: 4,
|
||||
})).toEqual({
|
||||
name: 'writer-key',
|
||||
rate_limit: 15,
|
||||
concurrent_limit: 4,
|
||||
})
|
||||
})
|
||||
})
|
||||
21
frontend/src/features/api-keys/utils/userKeyPayload.ts
Normal file
21
frontend/src/features/api-keys/utils/userKeyPayload.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface UserApiKeyMutationPayload {
|
||||
name: string
|
||||
rate_limit: number
|
||||
concurrent_limit?: number
|
||||
}
|
||||
|
||||
interface BuildUserApiKeyMutationPayloadInput {
|
||||
name: string
|
||||
rate_limit?: number
|
||||
concurrent_limit?: number
|
||||
}
|
||||
|
||||
export function buildUserApiKeyMutationPayload(
|
||||
input: BuildUserApiKeyMutationPayloadInput,
|
||||
): UserApiKeyMutationPayload {
|
||||
return {
|
||||
name: input.name,
|
||||
rate_limit: input.rate_limit ?? 0,
|
||||
...(input.concurrent_limit === undefined ? {} : { concurrent_limit: input.concurrent_limit }),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user