refactor: 重构限流系统和健康监控,支持按 API 格式区分

- 将 adaptive_concurrency 重命名为 adaptive_rpm,从并发控制改为 RPM 控制
- 健康监控器支持按 API 格式独立管理健康度和熔断器状态
- 新增 model_permissions 模块,支持按格式配置允许的模型
- 重构前端提供商相关表单组件,新增 Collapsible UI 组件
- 新增数据库迁移脚本支持新的数据结构
This commit is contained in:
fawney19
2026-01-10 18:43:53 +08:00
parent dd2fbf4424
commit 09e0f594ff
97 changed files with 6642 additions and 4169 deletions

View File

@@ -67,7 +67,6 @@ export interface GlobalModelExport {
export interface ProviderExport {
name: string
display_name: string
description?: string | null
website?: string | null
billing_type?: string | null
@@ -76,10 +75,13 @@ export interface ProviderExport {
rpm_limit?: number | null
provider_priority?: number
is_active: boolean
rate_limit?: number | null
concurrent_limit?: number | null
timeout?: number | null
max_retries?: number | null
proxy?: any
config?: any
endpoints: EndpointExport[]
api_keys: ProviderKeyExport[]
models: ModelExport[]
}
@@ -89,27 +91,26 @@ export interface EndpointExport {
headers?: any
timeout?: number
max_retries?: number
max_concurrent?: number | null
rate_limit?: number | null
is_active: boolean
custom_path?: string | null
config?: any
keys: KeyExport[]
proxy?: any
}
export interface KeyExport {
export interface ProviderKeyExport {
api_key: string
name?: string | null
note?: string | null
api_formats: string[]
rate_multiplier?: number
rate_multipliers?: Record<string, number> | null
internal_priority?: number
global_priority?: number | null
max_concurrent?: number | null
rate_limit?: number | null
daily_limit?: number | null
monthly_limit?: number | null
allowed_models?: string[] | null
rpm_limit?: number | null
allowed_models?: any
capabilities?: any
cache_ttl_minutes?: number
max_probe_interval_minutes?: number
is_active: boolean
}

View File

@@ -155,6 +155,7 @@ export interface RequestDetail {
request_body?: Record<string, any>
provider_request_headers?: Record<string, any>
response_headers?: Record<string, any>
client_response_headers?: Record<string, any>
response_body?: Record<string, any>
metadata?: Record<string, any>
// 阶梯计费信息

View File

@@ -14,7 +14,7 @@ export async function toggleAdaptiveMode(
message: string
key_id: string
is_adaptive: boolean
max_concurrent: number | null
rpm_limit: number | null
effective_limit: number | null
}> {
const response = await client.patch(`/api/admin/adaptive/keys/${keyId}/mode`, data)
@@ -22,16 +22,16 @@ export async function toggleAdaptiveMode(
}
/**
* 设置 Key 的固定并发限制
* 设置 Key 的固定 RPM 限制
*/
export async function setConcurrentLimit(
export async function setRpmLimit(
keyId: string,
limit: number
): Promise<{
message: string
key_id: string
is_adaptive: boolean
max_concurrent: number
rpm_limit: number
previous_mode: string
}> {
const response = await client.patch(`/api/admin/adaptive/keys/${keyId}/limit`, null, {

View File

@@ -27,15 +27,9 @@ export async function createEndpoint(
api_format: string
base_url: string
custom_path?: string
auth_type?: string
auth_header?: string
headers?: Record<string, string>
timeout?: number
max_retries?: number
priority?: number
weight?: number
max_concurrent?: number
rate_limit?: number
is_active?: boolean
config?: Record<string, any>
proxy?: ProxyConfig | null
@@ -52,16 +46,10 @@ export async function updateEndpoint(
endpointId: string,
data: Partial<{
base_url: string
custom_path: string
auth_type: string
auth_header: string
custom_path: string | null
headers: Record<string, string>
timeout: number
max_retries: number
priority: number
weight: number
max_concurrent: number
rate_limit: number
is_active: boolean
config: Record<string, any>
proxy: ProxyConfig | null
@@ -74,7 +62,7 @@ export async function updateEndpoint(
/**
* 删除 Endpoint
*/
export async function deleteEndpoint(endpointId: string): Promise<{ message: string; deleted_keys_count: number }> {
export async function deleteEndpoint(endpointId: string): Promise<{ message: string; affected_keys_count: number }> {
const response = await client.delete(`/api/admin/endpoints/${endpointId}`)
return response.data
}

View File

@@ -32,16 +32,21 @@ export async function getKeyHealth(keyId: string): Promise<HealthStatus> {
/**
* 恢复Key健康状态一键恢复重置健康度 + 关闭熔断器 + 取消自动禁用)
* @param keyId Key ID
* @param apiFormat 可选,指定 API 格式(如 CLAUDE、OPENAI不指定则恢复所有格式
*/
export async function recoverKeyHealth(keyId: string): Promise<{
export async function recoverKeyHealth(keyId: string, apiFormat?: string): Promise<{
message: string
details: {
api_format?: string
health_score: number
circuit_breaker_open: boolean
is_active: boolean
}
}> {
const response = await client.patch(`/api/admin/endpoints/health/keys/${keyId}`)
const response = await client.patch(`/api/admin/endpoints/health/keys/${keyId}`, null, {
params: apiFormat ? { api_format: apiFormat } : undefined
})
return response.data
}

View File

@@ -1,5 +1,5 @@
import client from '../client'
import type { EndpointAPIKey } from './types'
import type { EndpointAPIKey, AllowedModels } from './types'
/**
* 能力定义类型
@@ -49,67 +49,6 @@ export async function getModelCapabilities(modelName: string): Promise<ModelCapa
return response.data
}
/**
* 获取 Endpoint 的所有 Keys
*/
export async function getEndpointKeys(endpointId: string): Promise<EndpointAPIKey[]> {
const response = await client.get(`/api/admin/endpoints/${endpointId}/keys`)
return response.data
}
/**
* 为 Endpoint 添加 Key
*/
export async function addEndpointKey(
endpointId: string,
data: {
endpoint_id: string
api_key: string
name: string // 密钥名称(必填)
rate_multiplier?: number // 成本倍率(默认 1.0
internal_priority?: number // Endpoint 内部优先级(数字越小越优先)
max_concurrent?: number // 最大并发数(留空=自适应模式)
rate_limit?: number
daily_limit?: number
monthly_limit?: number
cache_ttl_minutes?: number // 缓存 TTL分钟0=禁用
max_probe_interval_minutes?: number // 熔断探测间隔(分钟)
allowed_models?: string[] // 允许使用的模型列表
capabilities?: Record<string, boolean> // 能力标签配置
note?: string // 备注说明(可选)
}
): Promise<EndpointAPIKey> {
const response = await client.post(`/api/admin/endpoints/${endpointId}/keys`, data)
return response.data
}
/**
* 更新 Endpoint Key
*/
export async function updateEndpointKey(
keyId: string,
data: Partial<{
api_key: string
name: string // 密钥名称
rate_multiplier: number // 成本倍率
internal_priority: number // Endpoint 内部优先级(提供商优先模式,数字越小越优先)
global_priority: number // 全局 Key 优先级(全局 Key 优先模式,数字越小越优先)
max_concurrent: number // 最大并发数(留空=自适应模式)
rate_limit: number
daily_limit: number
monthly_limit: number
cache_ttl_minutes: number // 缓存 TTL分钟0=禁用
max_probe_interval_minutes: number // 熔断探测间隔(分钟)
allowed_models: string[] | null // 允许使用的模型列表null 表示允许所有
capabilities: Record<string, boolean> | null // 能力标签配置
is_active: boolean
note: string // 备注说明
}>
): Promise<EndpointAPIKey> {
const response = await client.put(`/api/admin/endpoints/keys/${keyId}`, data)
return response.data
}
/**
* 获取完整的 API Key用于查看和复制
*/
@@ -119,22 +58,71 @@ export async function revealEndpointKey(keyId: string): Promise<{ api_key: strin
}
/**
* 删除 Endpoint Key
* 删除 Key
*/
export async function deleteEndpointKey(keyId: string): Promise<{ message: string }> {
const response = await client.delete(`/api/admin/endpoints/keys/${keyId}`)
return response.data
}
// ========== Provider 级别的 Keys API ==========
/**
* 批量更新 Endpoint Keys 的优先级(用于拖动排序)
* 获取 Provider 的所有 Keys
*/
export async function batchUpdateKeyPriority(
endpointId: string,
priorities: Array<{ key_id: string; internal_priority: number }>
): Promise<{ message: string; updated_count: number }> {
const response = await client.put(`/api/admin/endpoints/${endpointId}/keys/batch-priority`, {
priorities
})
export async function getProviderKeys(providerId: string): Promise<EndpointAPIKey[]> {
const response = await client.get(`/api/admin/endpoints/providers/${providerId}/keys`)
return response.data
}
/**
* 为 Provider 添加 Key
*/
export async function addProviderKey(
providerId: string,
data: {
api_formats: string[] // 支持的 API 格式列表(必填)
api_key: string
name: string
rate_multiplier?: number // 默认成本倍率
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
internal_priority?: number
rpm_limit?: number | null // RPM 限制(留空=自适应模式)
cache_ttl_minutes?: number
max_probe_interval_minutes?: number
allowed_models?: AllowedModels
capabilities?: Record<string, boolean>
note?: string
}
): Promise<EndpointAPIKey> {
const response = await client.post(`/api/admin/endpoints/providers/${providerId}/keys`, data)
return response.data
}
/**
* 更新 Key
*/
export async function updateProviderKey(
keyId: string,
data: Partial<{
api_formats: string[] // 支持的 API 格式列表
api_key: string
name: string
rate_multiplier: number // 默认成本倍率
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
internal_priority: number
global_priority: number | null
rpm_limit: number | null // RPM 限制(留空=自适应模式)
cache_ttl_minutes: number
max_probe_interval_minutes: number
allowed_models: AllowedModels
capabilities: Record<string, boolean> | null
is_active: boolean
note: string
}>
): Promise<EndpointAPIKey> {
const response = await client.put(`/api/admin/endpoints/keys/${keyId}`, data)
return response.data
}

View File

@@ -147,14 +147,26 @@ export async function queryProviderUpstreamModels(
/**
* 从上游提供商导入模型
* @param providerId 提供商 ID
* @param modelIds 模型 ID 列表
* @param options 可选配置
* @param options.tiered_pricing 阶梯计费配置
* @param options.price_per_request 按次计费价格
*/
export async function importModelsFromUpstream(
providerId: string,
modelIds: string[]
modelIds: string[],
options?: {
tiered_pricing?: object
price_per_request?: number
}
): Promise<ImportFromUpstreamResponse> {
const response = await client.post(
`/api/admin/providers/${providerId}/import-from-upstream`,
{ model_ids: modelIds }
{
model_ids: modelIds,
...options
}
)
return response.data
}

View File

@@ -1,5 +1,5 @@
import client from '../client'
import type { ProviderWithEndpointsSummary } from './types'
import type { ProviderWithEndpointsSummary, ProxyConfig } from './types'
/**
* 获取 Providers 摘要(包含 Endpoints 统计)
@@ -23,7 +23,7 @@ export async function getProvider(providerId: string): Promise<ProviderWithEndpo
export async function updateProvider(
providerId: string,
data: Partial<{
display_name: string
name: string
description: string
website: string
provider_priority: number
@@ -33,6 +33,10 @@ export async function updateProvider(
quota_last_reset_at: string // 周期开始时间
quota_expires_at: string
rpm_limit: number | null
// 请求配置(从 Endpoint 迁移)
timeout: number
max_retries: number
proxy: ProxyConfig | null
cache_ttl_minutes: number // 0表示不支持缓存>0表示支持缓存并设置TTL(分钟)
max_probe_interval_minutes: number
is_active: boolean
@@ -83,7 +87,6 @@ export interface TestModelResponse {
provider?: {
id: string
name: string
display_name: string
}
model?: string
}
@@ -92,4 +95,3 @@ export async function testModel(data: TestModelRequest): Promise<TestModelRespon
const response = await client.post('/api/admin/provider-query/test-model', data)
return response.data
}

View File

@@ -20,6 +20,38 @@ export const API_FORMAT_LABELS: Record<string, string> = {
[API_FORMATS.GEMINI_CLI]: 'Gemini CLI',
}
// API 格式缩写映射(用于空间紧凑的显示场景)
export const API_FORMAT_SHORT: Record<string, string> = {
[API_FORMATS.OPENAI]: 'O',
[API_FORMATS.OPENAI_CLI]: 'OC',
[API_FORMATS.CLAUDE]: 'C',
[API_FORMATS.CLAUDE_CLI]: 'CC',
[API_FORMATS.GEMINI]: 'G',
[API_FORMATS.GEMINI_CLI]: 'GC',
}
// API 格式排序顺序(统一的显示顺序)
export const API_FORMAT_ORDER: string[] = [
API_FORMATS.OPENAI,
API_FORMATS.OPENAI_CLI,
API_FORMATS.CLAUDE,
API_FORMATS.CLAUDE_CLI,
API_FORMATS.GEMINI,
API_FORMATS.GEMINI_CLI,
]
// 工具函数:按标准顺序排序 API 格式数组
export function sortApiFormats(formats: string[]): string[] {
return [...formats].sort((a, b) => {
const aIdx = API_FORMAT_ORDER.indexOf(a)
const bIdx = API_FORMAT_ORDER.indexOf(b)
if (aIdx === -1 && bIdx === -1) return 0
if (aIdx === -1) return 1
if (bIdx === -1) return -1
return aIdx - bIdx
})
}
/**
* 代理配置类型
*/
@@ -37,18 +69,9 @@ export interface ProviderEndpoint {
api_format: string
base_url: string
custom_path?: string // 自定义请求路径(可选,为空则使用 API 格式默认路径)
auth_type: string
auth_header?: string
headers?: Record<string, string>
timeout: number
max_retries: number
priority: number
weight: number
max_concurrent?: number
rate_limit?: number
health_score: number
consecutive_failures: number
last_failure_at?: string
is_active: boolean
config?: Record<string, any>
proxy?: ProxyConfig | null
@@ -58,25 +81,55 @@ export interface ProviderEndpoint {
updated_at: string
}
/**
* 模型权限配置类型(支持简单列表和按格式字典两种模式)
*
* 使用示例:
* 1. 不限制(允许所有模型): null
* 2. 简单列表模式(所有 API 格式共享同一个白名单): ["gpt-4", "claude-3-opus"]
* 3. 按格式字典模式(不同 API 格式使用不同的白名单):
* { "OPENAI": ["gpt-4"], "CLAUDE": ["claude-3-opus"] }
*/
export type AllowedModels = string[] | Record<string, string[]> | null
// AllowedModels 类型守卫函数
export function isAllowedModelsList(value: AllowedModels): value is string[] {
return Array.isArray(value)
}
export function isAllowedModelsDict(value: AllowedModels): value is Record<string, string[]> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false
}
// 验证所有值都是字符串数组
return Object.values(value).every(
(v) => Array.isArray(v) && v.every((item) => typeof item === 'string')
)
}
export interface EndpointAPIKey {
id: string
endpoint_id: string
provider_id: string
api_formats: string[] // 支持的 API 格式列表
api_key_masked: string
api_key_plain?: string | null
name: string // 密钥名称(必填,用于识别)
rate_multiplier: number // 成本倍率(真实成本 = 表面成本 × 倍率)
internal_priority: number // Endpoint 内部优先级
rate_multiplier: number // 默认成本倍率(真实成本 = 表面成本 × 倍率)
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率,如 {"CLAUDE": 1.0, "OPENAI": 0.8}
internal_priority: number // Key 内部优先级
global_priority?: number | null // 全局 Key 优先级
max_concurrent?: number
rate_limit?: number
daily_limit?: number
monthly_limit?: number
allowed_models?: string[] | null // 允许使用的模型列表null = 支持所有模型)
rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示自适应模式
allowed_models?: AllowedModels // 允许使用的模型列表null=不限制,列表=简单白名单,字典=按格式区分)
capabilities?: Record<string, boolean> | null // 能力标签配置(如 cache_1h, context_1m
// 缓存与熔断配置
cache_ttl_minutes: number // 缓存 TTL分钟0=禁用
max_probe_interval_minutes: number // 熔断探测间隔(分钟)
// 按格式的健康度数据
health_by_format?: Record<string, FormatHealthData>
circuit_breaker_by_format?: Record<string, FormatCircuitBreakerData>
// 聚合字段(从 health_by_format 计算,用于列表显示)
health_score: number
circuit_breaker_open?: boolean
consecutive_failures: number
last_failure_at?: string
request_count: number
@@ -89,10 +142,10 @@ export interface EndpointAPIKey {
last_used_at?: string
created_at: string
updated_at: string
// 自适应并发字段
is_adaptive?: boolean // 是否为自适应模式(max_concurrent=NULL
effective_limit?: number // 当前有效限制(自适应使用学习值,固定使用配置值)
learned_max_concurrent?: number
// 自适应 RPM 字段
is_adaptive?: boolean // 是否为自适应模式(rpm_limit=NULL
effective_limit?: number // 当前有效 RPM 限制(自适应使用学习值,固定使用配置值)
learned_rpm_limit?: number // 学习到的 RPM 限制
// 滑动窗口利用率采样
utilization_samples?: Array<{ ts: number; util: number }> // 利用率采样窗口
last_probe_increase_at?: string // 上次探测性扩容时间
@@ -100,8 +153,7 @@ export interface EndpointAPIKey {
rpm_429_count?: number
last_429_at?: string
last_429_type?: string
// 熔断器字段(滑动窗口 + 半开模式)
circuit_breaker_open?: boolean
// 单格式场景的熔断器字段
circuit_breaker_open_at?: string
next_probe_at?: string
half_open_until?: string
@@ -110,17 +162,36 @@ export interface EndpointAPIKey {
request_results_window?: Array<{ ts: number; ok: boolean }> // 请求结果滑动窗口
}
// 按格式的健康度数据
export interface FormatHealthData {
health_score: number
error_rate: number
window_size: number
consecutive_failures: number
last_failure_at?: string | null
circuit_breaker: FormatCircuitBreakerData
}
// 按格式的熔断器数据
export interface FormatCircuitBreakerData {
open: boolean
open_at?: string | null
next_probe_at?: string | null
half_open_until?: string | null
half_open_successes: number
half_open_failures: number
}
export interface EndpointAPIKeyUpdate {
api_formats?: string[] // 支持的 API 格式列表
name?: string
api_key?: string // 仅在需要更新时提供
rate_multiplier?: number
rate_multiplier?: number // 默认成本倍率
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
internal_priority?: number
global_priority?: number | null
max_concurrent?: number | null // null 表示切换为自适应模式
rate_limit?: number
daily_limit?: number
monthly_limit?: number
allowed_models?: string[] | null
rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示切换为自适应模式
allowed_models?: AllowedModels
capabilities?: Record<string, boolean> | null
cache_ttl_minutes?: number
max_probe_interval_minutes?: number
@@ -198,7 +269,6 @@ export interface PublicEndpointStatusMonitorResponse {
export interface ProviderWithEndpointsSummary {
id: string
name: string
display_name: string
description?: string
website?: string
provider_priority: number
@@ -208,9 +278,10 @@ export interface ProviderWithEndpointsSummary {
quota_reset_day?: number
quota_last_reset_at?: string // 当前周期开始时间
quota_expires_at?: string
rpm_limit?: number | null
rpm_used?: number
rpm_reset_at?: string
// 请求配置(从 Endpoint 迁移)
timeout?: number // 请求超时(秒)
max_retries?: number // 最大重试次数
proxy?: ProxyConfig | null // 代理配置
is_active: boolean
total_endpoints: number
active_endpoints: number
@@ -253,13 +324,10 @@ export interface HealthSummary {
}
}
export interface ConcurrencyStatus {
endpoint_id?: string
endpoint_current_concurrency: number
endpoint_max_concurrent?: number
key_id?: string
key_current_concurrency: number
key_max_concurrent?: number
export interface KeyRpmStatus {
key_id: string
current_rpm: number
rpm_limit?: number
}
export interface ProviderModelMapping {
@@ -361,7 +429,6 @@ export interface ModelPriceRange {
export interface ModelCatalogProviderDetail {
provider_id: string
provider_name: string
provider_display_name?: string | null
model_id?: string | null
target_model: string
input_price_per_1m?: number | null
@@ -534,10 +601,10 @@ export interface UpstreamModel {
*/
export interface ImportFromUpstreamSuccessItem {
model_id: string
global_model_id: string
global_model_name: string
provider_model_id: string
created_global_model: boolean
global_model_id?: string // 可选,未关联时为空字符串
global_model_name?: string // 可选,未关联时为空字符串
created_global_model: boolean // 始终为 false不再自动创建 GlobalModel
}
/**