perf: 全栈查询优化、前端缓存去重与页面可见性优化

后端:
- SQL count 查询统一改用 func.count() 子查询替代 query.count()
- Dashboard/Audit 等页面多次独立查询合并为单次聚合查询
- Provider summary 列表改为批量查询消除 N+1 问题
- DailyStats 逐天循环查询改为 CASE 分桶单次查询
- 使用 load_only() 减少不必要的列加载
- cache_decorator 支持嵌套属性路径解析(dotted vary_by)
- 多个管理/公共端点新增 @cache_result 缓存装饰

前端:
- cache.ts 新增 in-flight 请求复用、dedupedRequest、buildCacheKey
- 大量 API 调用添加前端缓存或去重
- 多个页面定时器在标签页隐藏时暂停、可见时恢复
- Auth 检查从 setInterval 改为 storage + visibilitychange 事件驱动
- 请求竞态防护(requestId 模式)

数据库:
- Usage 表新增 idx_usage_status_user_created 复合索引
This commit is contained in:
fawney19
2026-03-03 22:04:40 +08:00
parent 0a60492146
commit 97b0146ce9
66 changed files with 2306 additions and 657 deletions

View File

@@ -9,12 +9,14 @@ from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from sqlalchemy.orm import Session
from sqlalchemy import func
from sqlalchemy.orm import Session, load_only
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.models_service import invalidate_models_list_cache
from src.api.base.pipeline import ApiRequestPipeline
from src.config.constants import CacheTTL
from src.core.enums import ProviderBillingType
from src.core.exceptions import InvalidRequestException, NotFoundException
from src.core.logger import logger
@@ -27,6 +29,7 @@ from src.models.database import GlobalModel, Provider, ProviderAPIKey, ProviderE
from src.models.endpoint_models import ProviderWithEndpointsSummary
from src.services.cache.model_cache import ModelCacheService
from src.services.cache.provider_cache import ProviderCacheService
from src.utils.cache_decorator import cache_result
from .summary import _build_provider_summary
@@ -341,9 +344,24 @@ class AdminListProvidersAdapter(AdminApiAdapter):
self.limit = limit
self.is_active = is_active
@cache_result(
key_prefix="admin:providers:list",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
vary_by=["skip", "limit", "is_active"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
query = db.query(Provider)
query = db.query(Provider).options(
load_only(
Provider.id,
Provider.name,
Provider.provider_priority,
Provider.is_active,
Provider.created_at,
Provider.updated_at,
)
)
if self.is_active is not None:
query = query.filter(Provider.is_active == self.is_active)
providers = query.offset(self.skip).limit(self.limit).all()
@@ -723,11 +741,22 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
def __init__(self, provider_id: str):
self.provider_id = provider_id
async def handle(self, context: ApiRequestContext) -> ProviderMappingPreviewResponse: # type: ignore[override]
@cache_result(
key_prefix="admin:providers:mapping-preview",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
vary_by=["provider_id"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
# 获取 Provider
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
provider = (
db.query(Provider)
.options(load_only(Provider.id, Provider.name))
.filter(Provider.id == self.provider_id)
.first()
)
if not provider:
raise NotFoundException("提供商不存在", "provider")
@@ -735,19 +764,6 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
truncated_keys = 0
truncated_models = 0
# 获取该 Provider 有白名单配置的 Key 总数(用于截断统计)
from sqlalchemy import func
total_keys_with_allowed_models = (
db.query(func.count(ProviderAPIKey.id))
.filter(
ProviderAPIKey.provider_id == self.provider_id,
ProviderAPIKey.allowed_models.isnot(None),
)
.scalar()
or 0
)
# 获取该 Provider 有白名单配置的 Key只查询需要的字段
keys = (
db.query(
@@ -761,25 +777,22 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
ProviderAPIKey.provider_id == self.provider_id,
ProviderAPIKey.allowed_models.isnot(None),
)
.limit(MAPPING_PREVIEW_MAX_KEYS)
.limit(MAPPING_PREVIEW_MAX_KEYS + 1)
.all()
)
# 计算被截断的 Key 数量
if total_keys_with_allowed_models > MAPPING_PREVIEW_MAX_KEYS:
truncated_keys = total_keys_with_allowed_models - MAPPING_PREVIEW_MAX_KEYS
# 获取有 model_mappings 配置的 GlobalModel 总数(用于截断统计)
total_models_with_mappings = (
db.query(func.count(GlobalModel.id))
.filter(
GlobalModel.config.isnot(None),
GlobalModel.config["model_mappings"].isnot(None),
func.jsonb_array_length(GlobalModel.config["model_mappings"]) > 0,
if len(keys) > MAPPING_PREVIEW_MAX_KEYS:
keys = keys[:MAPPING_PREVIEW_MAX_KEYS]
total_keys_with_allowed_models = (
db.query(func.count(ProviderAPIKey.id))
.filter(
ProviderAPIKey.provider_id == self.provider_id,
ProviderAPIKey.allowed_models.isnot(None),
)
.scalar()
or 0
)
.scalar()
or 0
)
truncated_keys = total_keys_with_allowed_models - MAPPING_PREVIEW_MAX_KEYS
# 只查询有 model_mappings 配置的 GlobalModel使用 SQLAlchemy JSONB 操作符)
global_models = (
@@ -795,12 +808,22 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
GlobalModel.config["model_mappings"].isnot(None),
func.jsonb_array_length(GlobalModel.config["model_mappings"]) > 0,
)
.limit(MAPPING_PREVIEW_MAX_MODELS)
.limit(MAPPING_PREVIEW_MAX_MODELS + 1)
.all()
)
# 计算被截断的 GlobalModel 数量
if total_models_with_mappings > MAPPING_PREVIEW_MAX_MODELS:
if len(global_models) > MAPPING_PREVIEW_MAX_MODELS:
global_models = global_models[:MAPPING_PREVIEW_MAX_MODELS]
total_models_with_mappings = (
db.query(func.count(GlobalModel.id))
.filter(
GlobalModel.config.isnot(None),
GlobalModel.config["model_mappings"].isnot(None),
func.jsonb_array_length(GlobalModel.config["model_mappings"]) > 0,
)
.scalar()
or 0
)
truncated_models = total_models_with_mappings - MAPPING_PREVIEW_MAX_MODELS
# 构建有映射配置的 GlobalModel 映射
@@ -819,10 +842,10 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
keys=[],
total_keys=0,
total_matches=0,
truncated=False,
truncated_keys=0,
truncated_models=0,
)
truncated=truncated_keys > 0 or truncated_models > 0,
truncated_keys=truncated_keys,
truncated_models=truncated_models,
).model_dump()
key_infos: list[MappingMatchingKey] = []
total_matches = 0
@@ -837,18 +860,6 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
if not allowed_models_list:
continue
# 生成脱敏 Key
masked_key = "***"
if key.api_key:
try:
decrypted_key = crypto.decrypt(key.api_key, silent=True)
if len(decrypted_key) > 8:
masked_key = f"{decrypted_key[:4]}***{decrypted_key[-4:]}"
else:
masked_key = f"{decrypted_key[:2]}***"
except Exception:
pass
# 查找匹配的 GlobalModel
matching_global_models: list[MappingMatchingGlobalModel] = []
@@ -879,6 +890,18 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
total_matches += 1
if matching_global_models:
# 只有有匹配结果的 key 才做解密脱敏,减少 CPU 开销
masked_key = "***"
if key.api_key:
try:
decrypted_key = crypto.decrypt(key.api_key, silent=True)
if len(decrypted_key) > 8:
masked_key = f"{decrypted_key[:4]}***{decrypted_key[-4:]}"
else:
masked_key = f"{decrypted_key[:2]}***"
except Exception:
pass
key_infos.append(
MappingMatchingKey(
key_id=key.id or "",
@@ -901,7 +924,7 @@ class AdminGetProviderMappingPreviewAdapter(AdminApiAdapter):
truncated=is_truncated,
truncated_keys=truncated_keys,
truncated_models=truncated_models,
)
).model_dump()
# ========== Claude Code Pool Management ==========

View File

@@ -8,12 +8,13 @@ from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import case, func
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, load_only
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.models_service import invalidate_models_list_cache
from src.api.base.pipeline import ApiRequestPipeline
from src.config.constants import CacheTTL
from src.core.enums import ProviderBillingType
from src.core.exceptions import InvalidRequestException, NotFoundException
from src.core.logger import logger
@@ -39,6 +40,7 @@ from src.models.endpoint_models import (
)
from src.services.cache.model_cache import ModelCacheService
from src.services.cache.provider_cache import ProviderCacheService
from src.utils.cache_decorator import cache_result
router = APIRouter(tags=["Provider Summary"])
pipeline = ApiRequestPipeline()
@@ -130,11 +132,8 @@ async def get_provider_summary(
- `created_at`: 创建时间
- `updated_at`: 更新时间
"""
provider = db.query(Provider).filter(Provider.id == provider_id).first()
if not provider:
raise NotFoundException(f"Provider {provider_id} not found")
return _build_provider_summary(db, provider)
adapter = AdminProviderDetailAdapter(provider_id=provider_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{provider_id}/health-monitor", response_model=ProviderEndpointHealthMonitorResponse)
@@ -319,12 +318,20 @@ def _extract_failover_rules_from_config(
def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndpointsSummary:
endpoints = db.query(ProviderEndpoint).filter(ProviderEndpoint.provider_id == provider.id).all()
endpoints = (
db.query(ProviderEndpoint)
.options(
load_only(
ProviderEndpoint.id,
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.is_active,
)
)
.filter(ProviderEndpoint.provider_id == provider.id)
.all()
)
total_endpoints = len(endpoints)
active_endpoints = sum(1 for e in endpoints if e.is_active)
# Key 统计(合并为单个查询)
key_stats = (
db.query(
func.count(ProviderAPIKey.id).label("total"),
@@ -333,10 +340,9 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
.filter(ProviderAPIKey.provider_id == provider.id)
.first()
)
total_keys = key_stats.total or 0
total_keys = int(key_stats.total or 0)
active_keys = int(key_stats.active or 0)
# Model 统计(合并为单个查询)
model_stats = (
db.query(
func.count(Model.id).label("total"),
@@ -345,25 +351,62 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
.filter(Model.provider_id == provider.id)
.first()
)
total_models = model_stats.total or 0
total_models = int(model_stats.total or 0)
active_models = int(model_stats.active or 0)
# 活跃模型关联的全局模型 ID 列表
global_model_ids = [
row[0]
for row in db.query(Model.global_model_id)
.filter(
Model.provider_id == provider.id,
Model.is_active == True,
Model.global_model_id.isnot(None),
)
.distinct()
.all()
]
api_formats = [e.api_format for e in endpoints]
all_keys = (
db.query(ProviderAPIKey)
.options(
load_only(
ProviderAPIKey.id,
ProviderAPIKey.provider_id,
ProviderAPIKey.is_active,
ProviderAPIKey.api_formats,
ProviderAPIKey.health_by_format,
)
)
.filter(ProviderAPIKey.provider_id == provider.id)
.all()
)
# 优化: 一次性加载 Provider 的 keys避免 N+1 查询
all_keys = db.query(ProviderAPIKey).filter(ProviderAPIKey.provider_id == provider.id).all()
return _compose_provider_summary(
provider=provider,
endpoints=endpoints,
all_keys=all_keys,
total_keys=total_keys,
active_keys=active_keys,
total_models=total_models,
active_models=active_models,
global_model_ids=global_model_ids,
)
def _compose_provider_summary(
*,
provider: Provider,
endpoints: list[ProviderEndpoint],
all_keys: list[ProviderAPIKey],
total_keys: int,
active_keys: int,
total_models: int,
active_models: int,
global_model_ids: list[Any],
) -> ProviderWithEndpointsSummary:
total_endpoints = len(endpoints)
active_endpoints = sum(1 for e in endpoints if e.is_active)
api_formats = [e.api_format for e in endpoints]
# 按 api_formats 分组 keys通过 api_formats 关联)
format_to_endpoint_id: dict[str, str] = {e.api_format: e.id for e in endpoints}
@@ -379,9 +422,8 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
for endpoint in endpoints:
keys = keys_by_endpoint.get(endpoint.id, [])
if keys:
# 从 health_by_format 获取对应格式的健康度
api_fmt = endpoint.api_format
health_scores = []
health_scores: list[float] = []
for k in keys:
health_by_format = k.health_by_format or {}
if api_fmt in health_by_format:
@@ -389,7 +431,7 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
if score is not None:
health_scores.append(float(score))
else:
health_scores.append(1.0) # 默认健康度
health_scores.append(1.0)
avg_health = sum(health_scores) / len(health_scores) if health_scores else 1.0
endpoint_health_map[endpoint.id] = avg_health
else:
@@ -399,7 +441,6 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
avg_health_score = sum(all_health_scores) / len(all_health_scores) if all_health_scores else 1.0
unhealthy_endpoints = sum(1 for score in all_health_scores if score < 0.5)
# 计算每个端点的活跃密钥数量
active_keys_by_endpoint: dict[str, int] = {}
for endpoint_id, keys in keys_by_endpoint.items():
active_keys_by_endpoint[endpoint_id] = sum(1 for k in keys if k.is_active)
@@ -484,6 +525,119 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
)
def _build_provider_summaries_batch(
db: Session, providers: list[Provider]
) -> list[ProviderWithEndpointsSummary]:
if not providers:
return []
provider_ids = [provider.id for provider in providers]
endpoint_rows = (
db.query(ProviderEndpoint)
.options(
load_only(
ProviderEndpoint.id,
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.is_active,
)
)
.filter(ProviderEndpoint.provider_id.in_(provider_ids))
.all()
)
endpoints_by_provider: dict[str, list[ProviderEndpoint]] = {}
for endpoint in endpoint_rows:
endpoints_by_provider.setdefault(str(endpoint.provider_id), []).append(endpoint)
key_rows = (
db.query(ProviderAPIKey)
.options(
load_only(
ProviderAPIKey.id,
ProviderAPIKey.provider_id,
ProviderAPIKey.is_active,
ProviderAPIKey.api_formats,
ProviderAPIKey.health_by_format,
)
)
.filter(ProviderAPIKey.provider_id.in_(provider_ids))
.all()
)
keys_by_provider: dict[str, list[ProviderAPIKey]] = {}
for key in key_rows:
keys_by_provider.setdefault(str(key.provider_id), []).append(key)
key_stats_rows = (
db.query(
ProviderAPIKey.provider_id.label("provider_id"),
func.count(ProviderAPIKey.id).label("total"),
func.sum(case((ProviderAPIKey.is_active == True, 1), else_=0)).label("active"),
)
.filter(ProviderAPIKey.provider_id.in_(provider_ids))
.group_by(ProviderAPIKey.provider_id)
.all()
)
key_stats_by_provider: dict[str, dict[str, int]] = {
str(row.provider_id): {
"total": int(row.total or 0),
"active": int(row.active or 0),
}
for row in key_stats_rows
}
model_stats_rows = (
db.query(
Model.provider_id.label("provider_id"),
func.count(Model.id).label("total"),
func.sum(case((Model.is_active == True, 1), else_=0)).label("active"),
)
.filter(Model.provider_id.in_(provider_ids))
.group_by(Model.provider_id)
.all()
)
model_stats_by_provider: dict[str, dict[str, int]] = {
str(row.provider_id): {
"total": int(row.total or 0),
"active": int(row.active or 0),
}
for row in model_stats_rows
}
global_model_rows = (
db.query(Model.provider_id, Model.global_model_id)
.filter(
Model.provider_id.in_(provider_ids),
Model.is_active == True,
Model.global_model_id.isnot(None),
)
.distinct()
.all()
)
global_model_ids_by_provider: dict[str, list[Any]] = {}
for provider_id, global_model_id in global_model_rows:
global_model_ids_by_provider.setdefault(str(provider_id), []).append(global_model_id)
summaries: list[ProviderWithEndpointsSummary] = []
for provider in providers:
pid = str(provider.id)
key_stats = key_stats_by_provider.get(pid, {"total": 0, "active": 0})
model_stats = model_stats_by_provider.get(pid, {"total": 0, "active": 0})
summaries.append(
_compose_provider_summary(
provider=provider,
endpoints=endpoints_by_provider.get(pid, []),
all_keys=keys_by_provider.get(pid, []),
total_keys=key_stats["total"],
active_keys=key_stats["active"],
total_models=model_stats["total"],
active_models=model_stats["active"],
global_model_ids=global_model_ids_by_provider.get(pid, []),
)
)
return summaries
# -------- Adapters --------
@@ -493,6 +647,12 @@ class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
lookback_hours: int
per_endpoint_limit: int
@cache_result(
key_prefix="admin:providers:health-monitor",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
vary_by=["provider_id", "lookback_hours", "per_endpoint_limit"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
@@ -501,6 +661,14 @@ class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
endpoints = (
db.query(ProviderEndpoint)
.options(
load_only(
ProviderEndpoint.id,
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.is_active,
)
)
.filter(ProviderEndpoint.provider_id == self.provider_id)
.all()
)
@@ -508,7 +676,7 @@ class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
now = datetime.now(timezone.utc)
since = now - timedelta(hours=self.lookback_hours)
endpoint_ids = [endpoint.id for endpoint in endpoints]
endpoint_ids = [str(endpoint.id) for endpoint in endpoints]
if not endpoint_ids:
response = ProviderEndpointHealthMonitorResponse(
provider_id=provider.id,
@@ -522,46 +690,65 @@ class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
endpoint_count=0,
lookback_hours=self.lookback_hours,
)
return response
return response.model_dump()
limit_rows = max(200, self.per_endpoint_limit * max(1, len(endpoint_ids)) * 2)
attempts_query = (
ranked_attempts_subq = (
db.query(RequestCandidate)
.with_entities(
RequestCandidate.endpoint_id.label("endpoint_id"),
RequestCandidate.status.label("status"),
RequestCandidate.status_code.label("status_code"),
RequestCandidate.latency_ms.label("latency_ms"),
RequestCandidate.error_type.label("error_type"),
RequestCandidate.error_message.label("error_message"),
func.coalesce(
RequestCandidate.finished_at,
RequestCandidate.started_at,
RequestCandidate.created_at,
).label("event_timestamp"),
func.row_number()
.over(
partition_by=RequestCandidate.endpoint_id,
order_by=RequestCandidate.created_at.desc(),
)
.label("rn"),
)
.filter(
RequestCandidate.endpoint_id.in_(endpoint_ids),
RequestCandidate.created_at >= since,
)
.order_by(RequestCandidate.created_at.desc())
.subquery()
)
attempt_rows = (
db.query(ranked_attempts_subq)
.filter(ranked_attempts_subq.c.rn <= self.per_endpoint_limit)
.order_by(
ranked_attempts_subq.c.endpoint_id.asc(),
ranked_attempts_subq.c.event_timestamp.asc(),
)
.all()
)
attempts = attempts_query.limit(limit_rows).all()
buffered_attempts: dict[str, list[RequestCandidate]] = {eid: [] for eid in endpoint_ids}
counters: dict[str, int] = {eid: 0 for eid in endpoint_ids}
for attempt in attempts:
if not attempt.endpoint_id or attempt.endpoint_id not in buffered_attempts:
events_by_endpoint: dict[str, list[EndpointHealthEvent]] = {eid: [] for eid in endpoint_ids}
for row in attempt_rows:
endpoint_id = str(row.endpoint_id) if row.endpoint_id is not None else ""
if not endpoint_id or endpoint_id not in events_by_endpoint:
continue
if counters[attempt.endpoint_id] >= self.per_endpoint_limit:
continue
buffered_attempts[attempt.endpoint_id].append(attempt)
counters[attempt.endpoint_id] += 1
events_by_endpoint[endpoint_id].append(
EndpointHealthEvent(
timestamp=row.event_timestamp,
status=row.status,
status_code=row.status_code,
latency_ms=row.latency_ms,
error_type=row.error_type,
error_message=row.error_message,
)
)
endpoint_monitors: list[EndpointHealthMonitor] = []
for endpoint in endpoints:
attempt_list = list(reversed(buffered_attempts.get(endpoint.id, [])))
events: list[EndpointHealthEvent] = []
for attempt in attempt_list:
event_timestamp = attempt.finished_at or attempt.started_at or attempt.created_at
events.append(
EndpointHealthEvent(
timestamp=event_timestamp,
status=attempt.status,
status_code=attempt.status_code,
latency_ms=attempt.latency_ms,
error_type=attempt.error_type,
error_message=attempt.error_message,
)
)
endpoint_id = str(endpoint.id)
events = events_by_endpoint.get(endpoint_id, [])
success_count = sum(1 for event in events if event.status == "success")
failed_count = sum(1 for event in events if event.status == "failed")
@@ -598,10 +785,15 @@ class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
lookback_hours=self.lookback_hours,
per_endpoint_limit=self.per_endpoint_limit,
)
return response
return response.model_dump()
class AdminProviderSummaryAdapter(AdminApiAdapter):
@cache_result(
key_prefix="admin:providers:summary",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
providers = (
@@ -609,7 +801,25 @@ class AdminProviderSummaryAdapter(AdminApiAdapter):
.order_by(Provider.provider_priority.asc(), Provider.created_at.asc())
.all()
)
return [_build_provider_summary(db, provider) for provider in providers]
return [item.model_dump() for item in _build_provider_summaries_batch(db, providers)]
@dataclass
class AdminProviderDetailAdapter(AdminApiAdapter):
provider_id: str
@cache_result(
key_prefix="admin:providers:summary:detail",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
vary_by=["provider_id"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise NotFoundException(f"Provider {self.provider_id} not found")
return _build_provider_summary(db, provider).model_dump()
@dataclass