refactor: 引入 ExportMixin 统一模型导出逻辑,修复配置获取与 thinking 检测

- 新增 ExportMixin,基于排除列表自动收集可导出字段,应用于 Provider/Endpoint/GlobalModel/Model/ProviderAPIKey
- 修复系统配置获取:当 key 存在默认值时不再抛出 404
- 导出配置时增加 API Key / auth_config 解密失败的日志警告
- 修复 Gemini normalizer 的 thinking 检测,优先读取 internal.thinking 标准路径
This commit is contained in:
fawney19
2026-02-20 21:37:40 +08:00
parent 37758c2032
commit d1724caab4
3 changed files with 126 additions and 7 deletions

View File

@@ -587,7 +587,7 @@ class AdminGetSystemConfigAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
value = SystemConfigService.get_config(context.db, self.key) value = SystemConfigService.get_config(context.db, self.key)
if value is None: if value is None and self.key not in SystemConfigService.DEFAULT_CONFIGS:
raise NotFoundException(f"配置项 '{self.key}' 不存在") raise NotFoundException(f"配置项 '{self.key}' 不存在")
# 对敏感配置,只返回是否已设置的标志,不返回实际值 # 对敏感配置,只返回是否已设置的标志,不返回实际值
if self.key in self.SENSITIVE_KEYS: if self.key in self.SENSITIVE_KEYS:
@@ -894,6 +894,12 @@ class AdminExportConfigAdapter(AdminApiAdapter):
try: try:
key_data["api_key"] = crypto_service.decrypt(key.api_key) key_data["api_key"] = crypto_service.decrypt(key.api_key)
except Exception: except Exception:
logger.warning(
"API Key 解密失败: provider={}, key_id={}, api_formats={}",
provider.name,
key.id,
key.api_formats,
)
key_data["api_key"] = "" key_data["api_key"] = ""
# 解密 auth_configOAuth 等认证配置) # 解密 auth_configOAuth 等认证配置)
# 导出值为解密后的 JSON 字符串(非 dict导入时需按字符串重新加密 # 导出值为解密后的 JSON 字符串(非 dict导入时需按字符串重新加密
@@ -901,6 +907,11 @@ class AdminExportConfigAdapter(AdminApiAdapter):
try: try:
key_data["auth_config"] = crypto_service.decrypt(key.auth_config) key_data["auth_config"] = crypto_service.decrypt(key.auth_config)
except Exception: except Exception:
logger.warning(
"auth_config 解密失败: provider={}, key_id={}",
provider.name,
key.id,
)
pass # 解密失败则不导出 auth_config pass # 解密失败则不导出 auth_config
keys_data.append(key_data) keys_data.append(key_data)

View File

@@ -1846,6 +1846,10 @@ class GeminiNormalizer(FormatNormalizer):
def _is_antigravity_thinking_enabled(self, internal: InternalRequest) -> bool: def _is_antigravity_thinking_enabled(self, internal: InternalRequest) -> bool:
"""Best-effort detection of Claude-style `thinking` flag for Antigravity conversions.""" """Best-effort detection of Claude-style `thinking` flag for Antigravity conversions."""
try: try:
# 标准路径:跨格式转换时 ClaudeNormalizer 会将 thinking 写入 internal.thinking
if internal.thinking and internal.thinking.enabled:
return True
extra = internal.extra if isinstance(internal.extra, dict) else {} extra = internal.extra if isinstance(internal.extra, dict) else {}
claude_extra = extra.get("claude") claude_extra = extra.get("claude")
if not isinstance(claude_extra, dict): if not isinstance(claude_extra, dict):

View File

@@ -9,7 +9,7 @@ import secrets
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum as PyEnum from enum import Enum as PyEnum
from typing import Any from typing import Any, ClassVar
import bcrypt import bcrypt
from sqlalchemy import ( from sqlalchemy import (
@@ -39,6 +39,33 @@ from ..core.enums import AuthSource, ProviderBillingType, UserRole
Base = declarative_base() Base = declarative_base()
class ExportMixin:
"""配置导出 Mixin -- 基于排除列表自动收集字段。"""
_export_exclude: ClassVar[frozenset[str]] = frozenset()
def to_export_dict(self) -> dict[str, Any]:
"""将模型实例转为可导出的字典(排除 _export_exclude 中的字段)。"""
result: dict[str, Any] = {}
for col in self.__table__.columns: # type: ignore[attr-defined]
if col.name in self._export_exclude:
continue
value = getattr(self, col.name)
if isinstance(value, PyEnum):
value = value.value
result[col.name] = value
return result
@classmethod
def get_export_fields(cls) -> frozenset[str]:
"""返回可导出字段名集合。"""
return frozenset(
col.name
for col in cls.__table__.columns # type: ignore[attr-defined]
if col.name not in cls._export_exclude
)
class User(Base): class User(Base):
"""用户模型""" """用户模型"""
@@ -628,11 +655,22 @@ class UserOAuthLink(Base):
) )
class Provider(Base): class Provider(ExportMixin, Base):
"""提供商配置表""" """提供商配置表"""
__tablename__ = "providers" __tablename__ = "providers"
_export_exclude = frozenset(
{
"id",
"monthly_used_usd",
"quota_last_reset_at",
"quota_expires_at",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), unique=True, nullable=False, index=True) # 提供商名称(唯一) name = Column(String(100), unique=True, nullable=False, index=True) # 提供商名称(唯一)
description = Column(Text, nullable=True) # 提供商描述 description = Column(Text, nullable=True) # 提供商描述
@@ -728,11 +766,22 @@ class Provider(Base):
) )
class ProviderEndpoint(Base): class ProviderEndpoint(ExportMixin, Base):
"""提供商端点 - 一个提供商可以有多个 API 格式端点""" """提供商端点 - 一个提供商可以有多个 API 格式端点"""
__tablename__ = "provider_endpoints" __tablename__ = "provider_endpoints"
_export_exclude = frozenset(
{
"id",
"provider_id",
"api_family",
"endpoint_kind",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False) provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False)
@@ -882,7 +931,7 @@ class ProxyNode(Base):
__table_args__ = (UniqueConstraint("ip", "port", name="uq_proxy_node_ip_port"),) __table_args__ = (UniqueConstraint("ip", "port", name="uq_proxy_node_ip_port"),)
class GlobalModel(Base): class GlobalModel(ExportMixin, Base):
"""全局统一模型定义 - 包含价格和能力配置 """全局统一模型定义 - 包含价格和能力配置
设计原则: 设计原则:
@@ -893,6 +942,15 @@ class GlobalModel(Base):
__tablename__ = "global_models" __tablename__ = "global_models"
_export_exclude = frozenset(
{
"id",
"usage_count",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), unique=True, nullable=False, index=True) # 统一模型名(唯一) name = Column(String(100), unique=True, nullable=False, index=True) # 统一模型名(唯一)
display_name = Column(String(100), nullable=False) display_name = Column(String(100), nullable=False)
@@ -970,7 +1028,7 @@ class GlobalModel(Base):
models = relationship("Model", back_populates="global_model") models = relationship("Model", back_populates="global_model")
class Model(Base): class Model(ExportMixin, Base):
"""Provider 模型配置表 - Provider 如何使用某个 GlobalModel """Provider 模型配置表 - Provider 如何使用某个 GlobalModel
设计原则: 设计原则:
@@ -984,6 +1042,17 @@ class Model(Base):
__tablename__ = "models" __tablename__ = "models"
_export_exclude = frozenset(
{
"id",
"provider_id",
"global_model_id",
"is_available",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
provider_id = Column(String(36), ForeignKey("providers.id"), nullable=False) provider_id = Column(String(36), ForeignKey("providers.id"), nullable=False)
# 可为空NULL 表示未关联,不参与路由;非 NULL 表示已关联,参与路由 # 可为空NULL 表示未关联,不参与路由;非 NULL 表示已关联,参与路由
@@ -1368,11 +1437,46 @@ class DimensionCollector(Base):
) )
class ProviderAPIKey(Base): class ProviderAPIKey(ExportMixin, Base):
"""Provider API密钥表 - 直接归属于 Provider支持多种 API 格式""" """Provider API密钥表 - 直接归属于 Provider支持多种 API 格式"""
__tablename__ = "provider_api_keys" __tablename__ = "provider_api_keys"
_export_exclude = frozenset(
{
"id",
"provider_id",
"api_key",
"auth_config",
"learned_rpm_limit",
"concurrent_429_count",
"rpm_429_count",
"last_429_at",
"last_429_type",
"last_rpm_peak",
"adjustment_history",
"utilization_samples",
"last_probe_increase_at",
"health_by_format",
"circuit_breaker_by_format",
"request_count",
"success_count",
"error_count",
"total_response_time_ms",
"last_used_at",
"last_error_at",
"last_error_msg",
"expires_at",
"last_models_fetch_at",
"last_models_fetch_error",
"upstream_metadata",
"oauth_invalid_at",
"oauth_invalid_reason",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
# 外键关系 - 直接关联 Provider # 外键关系 - 直接关联 Provider