mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
fix: Codex 刷新限额功能优化
- 修复并发协程中直接操作 SQLAlchemy Session 的问题,改为收集后统一更新
- 抽取硬编码模型名为 CODEX_QUOTA_REFRESH_MODEL 常量
- 前端刷新限额前添加确认对话框,提示会产生 API 调用费用
- 修正 loguru 日志格式为 {} 占位符风格
This commit is contained in:
@@ -663,6 +663,7 @@ import Button from '@/components/ui/button.vue'
|
|||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
||||||
import { getProvider, getProviderEndpoints, updateProvider } from '@/api/endpoints'
|
import { getProvider, getProviderEndpoints, updateProvider } from '@/api/endpoints'
|
||||||
@@ -715,6 +716,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { error: showError, success: showSuccess } = useToast()
|
const { error: showError, success: showSuccess } = useToast()
|
||||||
|
const { confirm } = useConfirm()
|
||||||
const { copyToClipboard } = useClipboard()
|
const { copyToClipboard } = useClipboard()
|
||||||
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
|
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
|
||||||
|
|
||||||
@@ -1066,6 +1068,16 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
|||||||
// 刷新 Codex 所有账号限额
|
// 刷新 Codex 所有账号限额
|
||||||
async function handleRefreshQuota() {
|
async function handleRefreshQuota() {
|
||||||
if (refreshingQuota.value || !props.providerId) return
|
if (refreshingQuota.value || !props.providerId) return
|
||||||
|
|
||||||
|
// 确认对话框
|
||||||
|
const confirmed = await confirm({
|
||||||
|
title: '获取限额',
|
||||||
|
message: '这将使用每个账号发送测试请求以获取最新限额信息,会产生少量 API 调用费用。是否继续?',
|
||||||
|
confirmText: '继续',
|
||||||
|
variant: 'info'
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
refreshingQuota.value = true
|
refreshingQuota.value = true
|
||||||
try {
|
try {
|
||||||
const result = await refreshProviderQuota(props.providerId)
|
const result = await refreshProviderQuota(props.providerId)
|
||||||
|
|||||||
@@ -869,6 +869,9 @@ class AdminCreateProviderKeyAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
# ========== Codex Quota Refresh API ==========
|
# ========== Codex Quota Refresh API ==========
|
||||||
|
|
||||||
|
# Codex 限额刷新测试请求使用的模型(选择最小/最便宜的模型)
|
||||||
|
CODEX_QUOTA_REFRESH_MODEL = "gpt-5.1-codex-mini"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/providers/{provider_id}/refresh-quota")
|
@router.post("/providers/{provider_id}/refresh-quota")
|
||||||
async def refresh_provider_quota(
|
async def refresh_provider_quota(
|
||||||
@@ -906,7 +909,10 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from src.api.handlers.base.request_builder import get_provider_auth
|
from src.api.handlers.base.request_builder import get_provider_auth
|
||||||
from src.services.provider.metadata_collectors import MetadataCollectorRegistry, ensure_collectors_registered
|
from src.services.provider.metadata_collectors import (
|
||||||
|
MetadataCollectorRegistry,
|
||||||
|
ensure_collectors_registered,
|
||||||
|
)
|
||||||
from src.services.provider.transport import build_provider_url
|
from src.services.provider.transport import build_provider_url
|
||||||
from src.utils.ssl_utils import get_ssl_context
|
from src.utils.ssl_utils import get_ssl_context
|
||||||
|
|
||||||
@@ -933,7 +939,13 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not keys:
|
if not keys:
|
||||||
return {"success": 0, "failed": 0, "total": 0, "results": [], "message": "没有活跃的 Key"}
|
return {
|
||||||
|
"success": 0,
|
||||||
|
"failed": 0,
|
||||||
|
"total": 0,
|
||||||
|
"results": [],
|
||||||
|
"message": "没有活跃的 Key",
|
||||||
|
}
|
||||||
|
|
||||||
# 获取 openai:cli 端点
|
# 获取 openai:cli 端点
|
||||||
endpoint = None
|
endpoint = None
|
||||||
@@ -949,6 +961,9 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
success_count = 0
|
success_count = 0
|
||||||
failed_count = 0
|
failed_count = 0
|
||||||
|
|
||||||
|
# 用于收集需要更新的 key 元数据(避免在并发任务中直接操作 db session)
|
||||||
|
metadata_updates: dict[str, dict] = {} # key_id -> metadata
|
||||||
|
|
||||||
# 单个 Key 刷新函数
|
# 单个 Key 刷新函数
|
||||||
async def refresh_single_key(key: ProviderAPIKey) -> dict:
|
async def refresh_single_key(key: ProviderAPIKey) -> dict:
|
||||||
try:
|
try:
|
||||||
@@ -972,7 +987,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
# 发送最小的测试请求,使用 Codex Responses API 格式
|
# 发送最小的测试请求,使用 Codex Responses API 格式
|
||||||
test_body = {
|
test_body = {
|
||||||
"model": "gpt-5.1-codex-mini",
|
"model": CODEX_QUOTA_REFRESH_MODEL,
|
||||||
"input": [
|
"input": [
|
||||||
{
|
{
|
||||||
"type": "message",
|
"type": "message",
|
||||||
@@ -985,9 +1000,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
"store": False,
|
"store": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(timeout=30.0, verify=get_ssl_context()) as client:
|
||||||
timeout=30.0, verify=get_ssl_context()
|
|
||||||
) as client:
|
|
||||||
response = await client.post(url, json=test_body, headers=headers)
|
response = await client.post(url, json=test_body, headers=headers)
|
||||||
|
|
||||||
# 解析响应头中的限额信息
|
# 解析响应头中的限额信息
|
||||||
@@ -995,9 +1008,8 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
metadata = MetadataCollectorRegistry.collect("codex", response_headers)
|
metadata = MetadataCollectorRegistry.collect("codex", response_headers)
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
# 更新数据库中的元数据
|
# 收集元数据,稍后统一更新数据库
|
||||||
key.upstream_metadata = metadata
|
metadata_updates[key.id] = metadata
|
||||||
db.add(key)
|
|
||||||
return {
|
return {
|
||||||
"key_id": key.id,
|
"key_id": key.id,
|
||||||
"key_name": key.name,
|
"key_name": key.name,
|
||||||
@@ -1015,7 +1027,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"刷新 Key {key.id} 限额失败: {e}")
|
logger.error("刷新 Key {} 限额失败: {}", key.id, e)
|
||||||
return {
|
return {
|
||||||
"key_id": key.id,
|
"key_id": key.id,
|
||||||
"key_name": key.name,
|
"key_name": key.name,
|
||||||
@@ -1038,12 +1050,22 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|||||||
else:
|
else:
|
||||||
failed_count += 1
|
failed_count += 1
|
||||||
|
|
||||||
|
# 统一更新数据库(避免在并发任务中操作 session)
|
||||||
|
if metadata_updates:
|
||||||
|
for key in keys:
|
||||||
|
if key.id in metadata_updates:
|
||||||
|
key.upstream_metadata = metadata_updates[key.id]
|
||||||
|
db.add(key)
|
||||||
|
|
||||||
# 提交数据库更改
|
# 提交数据库更改
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[QUOTA_REFRESH] Provider {self.provider_id}: "
|
"[QUOTA_REFRESH] Provider {}: 成功 {}/{}, 失败 {}",
|
||||||
f"成功 {success_count}/{len(keys)}, 失败 {failed_count}"
|
self.provider_id,
|
||||||
|
success_count,
|
||||||
|
len(keys),
|
||||||
|
failed_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user