mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
@@ -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_config(OAuth 等认证配置)
|
# 解密 auth_config(OAuth 等认证配置)
|
||||||
# 导出值为解密后的 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user