mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
fix(providers): 修复 Key 表单状态同步与 streaming 候选状态码处理
前端: - KeyAllowedModelsEditDialog 同时监听 open 和 apiKey 变化,避免切换 key 时状态未刷新 - KeyFormDialog 新增 api_formats 过滤与默认值逻辑,可用格式变化时自动同步表单 - ProviderDetailDrawer 合并 provider 和 endpoint 的 api_formats 传递给 Key 表单, 数据刷新后同步 currentEndpoint 和 editingKey 引用 后端: - mark_candidate_streaming 移除 status_code 参数,streaming 阶段不再提前写入状态码 - 简化 active_requests 中 streaming 请求的完成判断,不再依赖 status_code 条件
This commit is contained in:
@@ -717,49 +717,44 @@ function parseAllowedModels(allowed: AllowedModels): string[] {
|
|||||||
return [...allowed]
|
return [...allowed]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听对话框打开
|
async function initializeDialogState(apiKey: EndpointAPIKey) {
|
||||||
watch(() => props.open, async (open) => {
|
loadingCancelled = false
|
||||||
if (open && props.apiKey) {
|
|
||||||
loadingCancelled = false
|
|
||||||
|
|
||||||
const parsed = parseAllowedModels(props.apiKey.allowed_models ?? null)
|
const parsed = parseAllowedModels(apiKey.allowed_models ?? null)
|
||||||
selectedModels.value = [...parsed]
|
selectedModels.value = [...parsed]
|
||||||
initialSelectedModels.value = [...parsed]
|
initialSelectedModels.value = [...parsed]
|
||||||
|
|
||||||
// 加载锁定的模型
|
const locked = apiKey.locked_models ?? []
|
||||||
const locked = props.apiKey.locked_models ?? []
|
lockedModels.value = [...locked]
|
||||||
lockedModels.value = [...locked]
|
initialLockedModels.value = [...locked]
|
||||||
initialLockedModels.value = [...locked]
|
|
||||||
|
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
upstreamModels.value = []
|
upstreamModels.value = []
|
||||||
upstreamModelsLoaded.value = false
|
upstreamModelsLoaded.value = false
|
||||||
allCustomModels.value = []
|
allCustomModels.value = []
|
||||||
|
|
||||||
// 自动获取模式下展开上游模型,其他收缩;非自动获取模式下全部展开
|
if (apiKey.auto_fetch_models) {
|
||||||
if (props.apiKey.auto_fetch_models) {
|
collapsedGroups.value = new Set(['global', 'custom'])
|
||||||
collapsedGroups.value = new Set(['global', 'custom'])
|
} else {
|
||||||
} else {
|
collapsedGroups.value = new Set()
|
||||||
collapsedGroups.value = new Set()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 加载该 Provider 已关联模型
|
await loadProviderModels()
|
||||||
await loadProviderModels()
|
|
||||||
|
|
||||||
// 自动获取模式下,获取上游模型用于显示(但选中状态使用已保存的 allowed_models)
|
if (apiKey.auto_fetch_models) {
|
||||||
if (props.apiKey.auto_fetch_models) {
|
await fetchUpstreamModels()
|
||||||
await fetchUpstreamModels()
|
}
|
||||||
// 注意:不再将所有上游模型自动标记为选中
|
|
||||||
// 因为后端有过滤规则,实际保存的 allowed_models 是过滤后的结果
|
|
||||||
// selectedModels 已在上面从 props.apiKey.allowed_models 初始化
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取自定义模型(不在提供商模型和上游模型中的)
|
const upstreamModelIdsSet = new Set(upstreamModels.value.map(m => m.id))
|
||||||
const upstreamModelIdsSet = new Set(upstreamModels.value.map(m => m.id))
|
allCustomModels.value = selectedModels.value.filter(m =>
|
||||||
// 自定义模型是用户手动添加的、不在已知模型列表中的
|
!providerModelNamesSet.value.has(m) && !upstreamModelIdsSet.has(m)
|
||||||
allCustomModels.value = selectedModels.value.filter(m =>
|
)
|
||||||
!providerModelNamesSet.value.has(m) && !upstreamModelIdsSet.has(m)
|
}
|
||||||
)
|
|
||||||
|
// 监听对话框打开 / 当前 key 变化
|
||||||
|
watch([() => props.open, () => props.apiKey], async ([open, apiKey]) => {
|
||||||
|
if (open && apiKey) {
|
||||||
|
await initializeDialogState(apiKey)
|
||||||
} else {
|
} else {
|
||||||
loadingCancelled = true
|
loadingCancelled = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,6 +377,32 @@ function normalizeApiFormat(format: string): string {
|
|||||||
return String(format || '').trim().toLowerCase()
|
return String(format || '').trim().toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAvailableApiFormatSet(): Set<string> {
|
||||||
|
return new Set(props.availableApiFormats.map(normalizeApiFormat))
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAvailableApiFormats(formats: string[]): string[] {
|
||||||
|
const availableFormatSet = getAvailableApiFormatSet()
|
||||||
|
if (availableFormatSet.size === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats.filter(format => availableFormatSet.has(normalizeApiFormat(format)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultApiFormats(): string[] {
|
||||||
|
const endpointFormat = props.endpoint?.api_format
|
||||||
|
if (endpointFormat) {
|
||||||
|
const endpointFormats = filterAvailableApiFormats([endpointFormat])
|
||||||
|
if (endpointFormats.length > 0) {
|
||||||
|
return endpointFormats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstAvailableFormat = sortApiFormats(props.availableApiFormats)[0]
|
||||||
|
return firstAvailableFormat ? [firstAvailableFormat] : []
|
||||||
|
}
|
||||||
|
|
||||||
// 按 provider/auth_type 过滤后的可用 API 格式列表
|
// 按 provider/auth_type 过滤后的可用 API 格式列表
|
||||||
const visibleApiFormats = computed(() => {
|
const visibleApiFormats = computed(() => {
|
||||||
const sorted = sortApiFormats(props.availableApiFormats)
|
const sorted = sortApiFormats(props.availableApiFormats)
|
||||||
@@ -486,6 +512,29 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => props.availableApiFormats, () => props.open, () => props.editingKey],
|
||||||
|
([, open, editingKey]) => {
|
||||||
|
if (!open) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = filterAvailableApiFormats(form.value.api_formats)
|
||||||
|
if (filtered.length !== form.value.api_formats.length) {
|
||||||
|
form.value.api_formats = [...filtered]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editingKey && form.value.api_formats.length === 0) {
|
||||||
|
const defaults = getDefaultApiFormats()
|
||||||
|
if (defaults.length > 0) {
|
||||||
|
form.value.api_formats = defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 加载能力列表
|
// 加载能力列表
|
||||||
async function loadCapabilities() {
|
async function loadCapabilities() {
|
||||||
try {
|
try {
|
||||||
@@ -540,7 +589,7 @@ function resetForm() {
|
|||||||
api_key: '',
|
api_key: '',
|
||||||
auth_type: defaultAuthType,
|
auth_type: defaultAuthType,
|
||||||
auth_config_text: '',
|
auth_config_text: '',
|
||||||
api_formats: [], // 默认不选中任何格式
|
api_formats: getDefaultApiFormats(),
|
||||||
rate_multipliers: {},
|
rate_multipliers: {},
|
||||||
internal_priority: 10,
|
internal_priority: 10,
|
||||||
rpm_limit: undefined,
|
rpm_limit: undefined,
|
||||||
@@ -573,7 +622,7 @@ function loadKeyData() {
|
|||||||
auth_type: props.editingKey.auth_type === 'service_account' ? 'service_account' : 'api_key',
|
auth_type: props.editingKey.auth_type === 'service_account' ? 'service_account' : 'api_key',
|
||||||
auth_config_text: '', // auth_config 不返回给前端,编辑时需要重新输入
|
auth_config_text: '', // auth_config 不返回给前端,编辑时需要重新输入
|
||||||
api_formats: props.editingKey.api_formats?.length > 0
|
api_formats: props.editingKey.api_formats?.length > 0
|
||||||
? [...props.editingKey.api_formats]
|
? filterAvailableApiFormats(props.editingKey.api_formats)
|
||||||
: [], // 编辑模式下保持原有选择,不默认全选
|
: [], // 编辑模式下保持原有选择,不默认全选
|
||||||
rate_multipliers: { ...(props.editingKey.rate_multipliers || {}) },
|
rate_multipliers: { ...(props.editingKey.rate_multipliers || {}) },
|
||||||
internal_priority: props.editingKey.internal_priority ?? 10,
|
internal_priority: props.editingKey.internal_priority ?? 10,
|
||||||
|
|||||||
@@ -936,7 +936,7 @@
|
|||||||
:editing-key="editingKey"
|
:editing-key="editingKey"
|
||||||
:provider-id="provider ? provider.id : null"
|
:provider-id="provider ? provider.id : null"
|
||||||
:provider-type="provider?.provider_type || null"
|
:provider-type="provider?.provider_type || null"
|
||||||
:available-api-formats="provider?.api_formats || []"
|
:available-api-formats="availableKeyApiFormats"
|
||||||
@close="keyFormDialogOpen = false"
|
@close="keyFormDialogOpen = false"
|
||||||
@saved="handleKeyChanged"
|
@saved="handleKeyChanged"
|
||||||
/>
|
/>
|
||||||
@@ -1258,6 +1258,65 @@ const allKeys = computed(() => {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const availableKeyApiFormats = computed(() => {
|
||||||
|
const formatSet = new Set<string>()
|
||||||
|
|
||||||
|
for (const format of provider.value?.api_formats || []) {
|
||||||
|
if (format) {
|
||||||
|
formatSet.add(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const endpoint of endpoints.value) {
|
||||||
|
if (endpoint.api_format) {
|
||||||
|
formatSet.add(endpoint.api_format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortApiFormats([...formatSet])
|
||||||
|
})
|
||||||
|
|
||||||
|
function syncCurrentSelections(
|
||||||
|
nextEndpoints: ProviderEndpointWithKeys[] = endpoints.value,
|
||||||
|
nextProviderKeys: EndpointAPIKey[] = providerKeys.value
|
||||||
|
) {
|
||||||
|
if (currentEndpoint.value) {
|
||||||
|
currentEndpoint.value = nextEndpoints.find(endpoint => endpoint.id === currentEndpoint.value?.id) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editingKey.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestKeys: EndpointAPIKey[] = []
|
||||||
|
const seenKeyIds = new Set<string>()
|
||||||
|
|
||||||
|
for (const key of nextProviderKeys) {
|
||||||
|
if (!seenKeyIds.has(key.id)) {
|
||||||
|
seenKeyIds.add(key.id)
|
||||||
|
latestKeys.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const endpoint of nextEndpoints) {
|
||||||
|
for (const key of endpoint.keys || []) {
|
||||||
|
if (!seenKeyIds.has(key.id)) {
|
||||||
|
seenKeyIds.add(key.id)
|
||||||
|
latestKeys.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestEditingKey = latestKeys.find(key => key.id === editingKey.value?.id) || null
|
||||||
|
editingKey.value = latestEditingKey
|
||||||
|
|
||||||
|
if (!latestEditingKey) {
|
||||||
|
keyFormDialogOpen.value = false
|
||||||
|
keyPermissionsDialogOpen.value = false
|
||||||
|
oauthKeyEditDialogOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 账号列表智能分页 =====
|
// ===== 账号列表智能分页 =====
|
||||||
const keysListRef = ref<HTMLElement | null>(null)
|
const keysListRef = ref<HTMLElement | null>(null)
|
||||||
const {
|
const {
|
||||||
@@ -1558,6 +1617,7 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
|||||||
const freshKeys = await getProviderKeys(props.providerId).catch(() => null)
|
const freshKeys = await getProviderKeys(props.providerId).catch(() => null)
|
||||||
if (freshKeys) {
|
if (freshKeys) {
|
||||||
providerKeys.value = freshKeys
|
providerKeys.value = freshKeys
|
||||||
|
syncCurrentSelections(endpoints.value, freshKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
||||||
@@ -2640,7 +2700,7 @@ async function loadEndpoints() {
|
|||||||
providerKeys.value = providerKeysResult
|
providerKeys.value = providerKeysResult
|
||||||
providerModels.value = modelsResult
|
providerModels.value = modelsResult
|
||||||
// 按 API 格式排序
|
// 按 API 格式排序
|
||||||
endpoints.value = endpointsList.sort((a, b) => {
|
const sortedEndpoints = endpointsList.sort((a, b) => {
|
||||||
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
||||||
const bIdx = API_FORMAT_ORDER.indexOf(b.api_format)
|
const bIdx = API_FORMAT_ORDER.indexOf(b.api_format)
|
||||||
if (aIdx === -1 && bIdx === -1) return 0
|
if (aIdx === -1 && bIdx === -1) return 0
|
||||||
@@ -2648,6 +2708,8 @@ async function loadEndpoints() {
|
|||||||
if (bIdx === -1) return -1
|
if (bIdx === -1) return -1
|
||||||
return aIdx - bIdx
|
return aIdx - bIdx
|
||||||
})
|
})
|
||||||
|
endpoints.value = sortedEndpoints
|
||||||
|
syncCurrentSelections(sortedEndpoints, providerKeysResult)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (requestId !== endpointsLoadRequestId) return
|
if (requestId !== endpointsLoadRequestId) return
|
||||||
showError(parseApiError(err, '加载端点失败'), '错误')
|
showError(parseApiError(err, '加载端点失败'), '错误')
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ class RequestCandidateService:
|
|||||||
def mark_candidate_streaming(
|
def mark_candidate_streaming(
|
||||||
db: Session,
|
db: Session,
|
||||||
candidate_id: str,
|
candidate_id: str,
|
||||||
status_code: int = 200,
|
|
||||||
concurrent_requests: int | None = None,
|
concurrent_requests: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -123,18 +122,19 @@ class RequestCandidateService:
|
|||||||
用于流式请求:连接建立成功后,流开始传输时调用。
|
用于流式请求:连接建立成功后,流开始传输时调用。
|
||||||
此时请求尚未完成,需要等流传输完毕后再调用 mark_candidate_success。
|
此时请求尚未完成,需要等流传输完毕后再调用 mark_candidate_success。
|
||||||
|
|
||||||
|
注意:streaming 阶段不设置 status_code,最终状态码由
|
||||||
|
mark_candidate_success / mark_candidate_failed 在流结束时写入。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: 数据库会话
|
db: 数据库会话
|
||||||
candidate_id: 候选ID
|
candidate_id: 候选ID
|
||||||
status_code: HTTP 状态码(通常是 200)
|
|
||||||
concurrent_requests: 并发请求数
|
concurrent_requests: 并发请求数
|
||||||
"""
|
"""
|
||||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||||
if candidate:
|
if candidate:
|
||||||
candidate.status = "streaming"
|
candidate.status = "streaming"
|
||||||
candidate.status_code = status_code
|
|
||||||
candidate.concurrent_requests = concurrent_requests
|
candidate.concurrent_requests = concurrent_requests
|
||||||
# streaming 状态不设置 finished_at,因为请求还在进行中
|
# streaming 状态不设置 finished_at 和 status_code,因为请求还在进行中
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ class RequestExecutor:
|
|||||||
RequestCandidateService.mark_candidate_streaming(
|
RequestCandidateService.mark_candidate_streaming(
|
||||||
db=self.db,
|
db=self.db,
|
||||||
candidate_id=candidate_id,
|
candidate_id=candidate_id,
|
||||||
status_code=200,
|
|
||||||
concurrent_requests=key_rpm_count,
|
concurrent_requests=key_rpm_count,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class UsageActiveRequestsMixin:
|
|||||||
|
|
||||||
通过 RequestCandidate 表判断哪些请求实际已成功完成:
|
通过 RequestCandidate 表判断哪些请求实际已成功完成:
|
||||||
1. status='success' 且 stream_completed=True(正常完成)
|
1. status='success' 且 stream_completed=True(正常完成)
|
||||||
2. status='streaming' 且 status_code=200(Provider 已返回成功,流因重启中断)
|
2. status='streaming'(Provider 已返回成功响应头,流因重启中断)
|
||||||
"""
|
"""
|
||||||
if not request_ids:
|
if not request_ids:
|
||||||
return set()
|
return set()
|
||||||
@@ -35,15 +35,13 @@ class UsageActiveRequestsMixin:
|
|||||||
db.query(
|
db.query(
|
||||||
RequestCandidate.request_id,
|
RequestCandidate.request_id,
|
||||||
RequestCandidate.status,
|
RequestCandidate.status,
|
||||||
RequestCandidate.status_code,
|
|
||||||
RequestCandidate.extra_data,
|
RequestCandidate.extra_data,
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
RequestCandidate.request_id.in_(request_ids),
|
RequestCandidate.request_id.in_(request_ids),
|
||||||
or_(
|
or_(
|
||||||
RequestCandidate.status == "success",
|
RequestCandidate.status == "success",
|
||||||
(RequestCandidate.status == "streaming")
|
RequestCandidate.status == "streaming",
|
||||||
& (RequestCandidate.status_code == 200),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
@@ -53,7 +51,7 @@ class UsageActiveRequestsMixin:
|
|||||||
extra_data = c.extra_data or {}
|
extra_data = c.extra_data or {}
|
||||||
if c.status == "success" and extra_data.get("stream_completed", False):
|
if c.status == "success" and extra_data.get("stream_completed", False):
|
||||||
completed.add(c.request_id)
|
completed.add(c.request_id)
|
||||||
elif c.status == "streaming" and c.status_code == 200:
|
elif c.status == "streaming":
|
||||||
completed.add(c.request_id)
|
completed.add(c.request_id)
|
||||||
return completed
|
return completed
|
||||||
|
|
||||||
@@ -111,7 +109,7 @@ class UsageActiveRequestsMixin:
|
|||||||
清理超时的 pending/streaming 请求
|
清理超时的 pending/streaming 请求
|
||||||
|
|
||||||
将超过指定时间仍处于 pending 或 streaming 状态的请求标记为 failed 或恢复为 completed。
|
将超过指定时间仍处于 pending 或 streaming 状态的请求标记为 failed 或恢复为 completed。
|
||||||
会检查 RequestCandidate 表,如果 Provider 已返回成功(status_code=200),
|
会检查 RequestCandidate 表,如果 Provider 已返回成功响应(status=streaming 或 stream_completed),
|
||||||
则恢复为 completed 而非标记为 failed,同时同步更新 candidate 状态。
|
则恢复为 completed 而非标记为 failed,同时同步更新 candidate 状态。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
Reference in New Issue
Block a user