2026-02-28 14:09:36 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Provider Key 响应对象构建器。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
2026-03-20 19:16:52 +08:00
|
|
|
|
from dataclasses import asdict
|
2026-03-18 20:58:33 +08:00
|
|
|
|
from typing import Any
|
2026-02-28 14:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
from src.core.crypto import crypto_service
|
|
|
|
|
|
from src.core.logger import logger
|
2026-03-11 21:37:08 +08:00
|
|
|
|
from src.core.provider_oauth_utils import normalize_oauth_organizations
|
2026-02-28 14:09:36 +08:00
|
|
|
|
from src.models.database import ProviderAPIKey
|
|
|
|
|
|
from src.models.endpoint_models import EndpointAPIKeyResponse
|
|
|
|
|
|
from src.services.provider_keys.auth_type import normalize_auth_type
|
2026-03-20 19:16:52 +08:00
|
|
|
|
from src.services.provider_keys.status_snapshot_store import (
|
|
|
|
|
|
normalize_oauth_expires_at,
|
|
|
|
|
|
resolve_provider_key_status_snapshot,
|
|
|
|
|
|
)
|
2026-02-28 14:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_key_response(
|
2026-03-20 19:16:52 +08:00
|
|
|
|
key: ProviderAPIKey,
|
|
|
|
|
|
api_key_plain: str | None = None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
provider_type: str | None = None,
|
2026-02-28 14:09:36 +08:00
|
|
|
|
) -> EndpointAPIKeyResponse:
|
|
|
|
|
|
"""构建 Key 响应对象。"""
|
|
|
|
|
|
auth_type = normalize_auth_type(getattr(key, "auth_type", "api_key"))
|
2026-03-18 20:58:33 +08:00
|
|
|
|
encrypted_api_key = str(getattr(key, "api_key", "") or "")
|
|
|
|
|
|
request_count = int(getattr(key, "request_count", 0) or 0)
|
|
|
|
|
|
success_count = int(getattr(key, "success_count", 0) or 0)
|
|
|
|
|
|
total_response_time_ms = float(getattr(key, "total_response_time_ms", 0) or 0.0)
|
|
|
|
|
|
rpm_limit = getattr(key, "rpm_limit", None)
|
2026-02-28 14:09:36 +08:00
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
if auth_type in ("service_account", "vertex_ai"):
|
|
|
|
|
|
# Service Account 不显示占位符
|
2026-02-28 14:09:36 +08:00
|
|
|
|
masked_key = "[Service Account]"
|
|
|
|
|
|
elif auth_type == "oauth":
|
|
|
|
|
|
masked_key = "[OAuth Token]"
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
2026-03-18 20:58:33 +08:00
|
|
|
|
decrypted_key = crypto_service.decrypt(encrypted_api_key)
|
2026-02-28 14:09:36 +08:00
|
|
|
|
masked_key = f"{decrypted_key[:8]}***{decrypted_key[-4:]}"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
masked_key = "***ERROR***"
|
|
|
|
|
|
|
2026-03-18 20:58:33 +08:00
|
|
|
|
success_rate = success_count / request_count if request_count > 0 else 0.0
|
2026-03-20 19:16:52 +08:00
|
|
|
|
avg_response_time_ms = total_response_time_ms / success_count if success_count > 0 else 0.0
|
2026-02-28 14:09:36 +08:00
|
|
|
|
|
2026-03-18 20:58:33 +08:00
|
|
|
|
is_adaptive = rpm_limit is None
|
|
|
|
|
|
key_dict: dict[str, Any] = dict(getattr(key, "__dict__", {}))
|
2026-02-28 14:09:36 +08:00
|
|
|
|
key_dict.pop("_sa_instance_state", None)
|
|
|
|
|
|
key_dict.pop("api_key", None) # 移除敏感字段,避免泄露
|
|
|
|
|
|
key_dict["auth_type"] = auth_type
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 OAuth 元数据(如果是 OAuth 类型)
|
|
|
|
|
|
oauth_expires_at = None
|
|
|
|
|
|
oauth_email = None
|
|
|
|
|
|
oauth_plan_type = None
|
|
|
|
|
|
oauth_account_id = None
|
2026-03-18 20:58:33 +08:00
|
|
|
|
oauth_account_name = None
|
2026-03-11 21:37:08 +08:00
|
|
|
|
oauth_account_user_id = None
|
2026-03-20 19:16:52 +08:00
|
|
|
|
auth_config: dict[str, Any] | None = None
|
2026-03-11 21:37:08 +08:00
|
|
|
|
oauth_organizations: list[dict[str, object]] = []
|
2026-02-28 14:09:36 +08:00
|
|
|
|
encrypted_auth_config = key_dict.pop("auth_config", None) # 移除敏感字段,避免泄露
|
2026-03-20 19:16:52 +08:00
|
|
|
|
if auth_type == "oauth" and isinstance(encrypted_auth_config, str) and encrypted_auth_config:
|
2026-02-28 14:09:36 +08:00
|
|
|
|
try:
|
|
|
|
|
|
decrypted_config = crypto_service.decrypt(encrypted_auth_config)
|
|
|
|
|
|
auth_config = json.loads(decrypted_config)
|
2026-03-20 19:16:52 +08:00
|
|
|
|
oauth_expires_at = normalize_oauth_expires_at(auth_config.get("expires_at"))
|
2026-02-28 14:09:36 +08:00
|
|
|
|
oauth_email = auth_config.get("email")
|
2026-03-20 19:16:52 +08:00
|
|
|
|
oauth_plan_type = auth_config.get("plan_type") # Codex: plus/free/team/enterprise
|
2026-02-28 14:09:36 +08:00
|
|
|
|
# Antigravity 使用 "tier" 字段(如 "PAID"/"FREE"),做小写化 fallback
|
|
|
|
|
|
if not oauth_plan_type:
|
|
|
|
|
|
ag_tier = auth_config.get("tier")
|
|
|
|
|
|
if ag_tier and isinstance(ag_tier, str):
|
|
|
|
|
|
oauth_plan_type = ag_tier.lower()
|
2026-03-20 19:16:52 +08:00
|
|
|
|
oauth_account_id = auth_config.get("account_id") # Codex: chatgpt_account_id
|
2026-03-18 20:58:33 +08:00
|
|
|
|
oauth_account_name = auth_config.get("account_name")
|
2026-03-11 21:37:08 +08:00
|
|
|
|
oauth_account_user_id = auth_config.get("account_user_id")
|
2026-03-20 19:16:52 +08:00
|
|
|
|
oauth_organizations = normalize_oauth_organizations(auth_config.get("organizations"))
|
2026-02-28 14:09:36 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error("Failed to decrypt auth_config for key {}: {}", key.id, e)
|
|
|
|
|
|
|
2026-03-20 19:16:52 +08:00
|
|
|
|
if not provider_type:
|
|
|
|
|
|
provider_rel = getattr(key, "provider", None)
|
|
|
|
|
|
provider_type = (
|
|
|
|
|
|
str(getattr(provider_rel, "provider_type", None) or "").strip()
|
|
|
|
|
|
or str(getattr(provider_rel, "type", None) or "").strip()
|
|
|
|
|
|
or None
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
status_snapshot = resolve_provider_key_status_snapshot(
|
|
|
|
|
|
key,
|
|
|
|
|
|
provider_type=provider_type,
|
|
|
|
|
|
auth_config=auth_config,
|
|
|
|
|
|
oauth_expires_at=oauth_expires_at,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-28 14:09:36 +08:00
|
|
|
|
# 从 health_by_format 计算汇总字段(便于列表展示)
|
2026-03-18 20:58:33 +08:00
|
|
|
|
raw_health_by_format = getattr(key, "health_by_format", None)
|
2026-03-20 19:16:52 +08:00
|
|
|
|
health_by_format = raw_health_by_format if isinstance(raw_health_by_format, dict) else {}
|
2026-03-18 20:58:33 +08:00
|
|
|
|
raw_circuit_by_format = getattr(key, "circuit_breaker_by_format", None)
|
2026-03-20 19:16:52 +08:00
|
|
|
|
circuit_by_format = raw_circuit_by_format if isinstance(raw_circuit_by_format, dict) else {}
|
2026-02-28 14:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
# 计算整体健康度(取所有格式中的最低值)
|
|
|
|
|
|
if health_by_format:
|
2026-03-20 19:16:52 +08:00
|
|
|
|
health_scores = [float(h.get("health_score") or 1.0) for h in health_by_format.values()]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
min_health_score = min(health_scores) if health_scores else 1.0
|
|
|
|
|
|
# 取最大的连续失败次数
|
|
|
|
|
|
max_consecutive = max(
|
2026-03-20 19:16:52 +08:00
|
|
|
|
(int(h.get("consecutive_failures") or 0) for h in health_by_format.values()),
|
2026-02-28 14:09:36 +08:00
|
|
|
|
default=0,
|
|
|
|
|
|
)
|
|
|
|
|
|
# 取最近的失败时间
|
|
|
|
|
|
failure_times = [
|
2026-03-20 19:16:52 +08:00
|
|
|
|
h.get("last_failure_at") for h in health_by_format.values() if h.get("last_failure_at")
|
2026-02-28 14:09:36 +08:00
|
|
|
|
]
|
|
|
|
|
|
last_failure = max(failure_times) if failure_times else None
|
|
|
|
|
|
else:
|
|
|
|
|
|
min_health_score = 1.0
|
|
|
|
|
|
max_consecutive = 0
|
|
|
|
|
|
last_failure = None
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否有任何格式的熔断器打开
|
|
|
|
|
|
any_circuit_open = any(c.get("open", False) for c in circuit_by_format.values())
|
|
|
|
|
|
|
|
|
|
|
|
key_dict.update(
|
|
|
|
|
|
{
|
|
|
|
|
|
"api_key_masked": masked_key,
|
|
|
|
|
|
"api_key_plain": api_key_plain,
|
|
|
|
|
|
"success_rate": success_rate,
|
|
|
|
|
|
"avg_response_time_ms": round(avg_response_time_ms, 2),
|
|
|
|
|
|
"is_adaptive": is_adaptive,
|
|
|
|
|
|
"effective_limit": (
|
2026-03-18 20:58:33 +08:00
|
|
|
|
getattr(
|
|
|
|
|
|
key, "learned_rpm_limit", None
|
|
|
|
|
|
) # 自适应模式:使用学习值,未学习时为 None(不限制)
|
2026-02-28 14:09:36 +08:00
|
|
|
|
if is_adaptive
|
2026-03-18 20:58:33 +08:00
|
|
|
|
else rpm_limit
|
2026-02-28 14:09:36 +08:00
|
|
|
|
),
|
|
|
|
|
|
# 汇总字段
|
|
|
|
|
|
"health_score": min_health_score,
|
|
|
|
|
|
"consecutive_failures": max_consecutive,
|
|
|
|
|
|
"last_failure_at": last_failure,
|
|
|
|
|
|
"circuit_breaker_open": any_circuit_open,
|
|
|
|
|
|
# OAuth 相关
|
|
|
|
|
|
"oauth_expires_at": oauth_expires_at,
|
|
|
|
|
|
"oauth_email": oauth_email,
|
|
|
|
|
|
"oauth_plan_type": oauth_plan_type,
|
|
|
|
|
|
"oauth_account_id": oauth_account_id,
|
2026-03-18 20:58:33 +08:00
|
|
|
|
"oauth_account_name": oauth_account_name,
|
2026-03-11 21:37:08 +08:00
|
|
|
|
"oauth_account_user_id": oauth_account_user_id,
|
|
|
|
|
|
"oauth_organizations": oauth_organizations,
|
2026-03-20 19:16:52 +08:00
|
|
|
|
"oauth_invalid_at": status_snapshot.oauth.invalid_at,
|
2026-03-18 20:58:33 +08:00
|
|
|
|
"oauth_invalid_reason": getattr(key, "oauth_invalid_reason", None),
|
2026-03-20 19:16:52 +08:00
|
|
|
|
"status_snapshot": asdict(status_snapshot),
|
2026-02-28 14:09:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 防御性:确保 api_formats 存在(历史数据可能为空/缺失)
|
|
|
|
|
|
if "api_formats" not in key_dict or key_dict["api_formats"] is None:
|
|
|
|
|
|
key_dict["api_formats"] = []
|
|
|
|
|
|
|
|
|
|
|
|
return EndpointAPIKeyResponse(**key_dict)
|