mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 缓存计费细分、能力匹配优化、用户模型调用计数
1. 缓存创建 tokens 区分 5min/1h TTL,支持按缓存时长差异化计费 - Usage 表新增 cache_creation_input_tokens_5m/1h 字段 - Claude handler 解析新格式 (ephemeral_5m/1h, claude_cache_creation_5/1h) - 计费规则支持 cache_ttl_pricing 覆盖 cache_creation 价格 2. 能力匹配机制优化 - COMPATIBLE 能力不再硬过滤,改为排序阶段通过 capability_miss_count 优先级处理 - cache_1h 改为 COMPATIBLE + REQUEST_PARAM(自动检测请求体中的 ttl=1h) - gemini_files 改为 EXCLUSIVE + REQUEST_PARAM(自动检测 fileData.fileUri) - 移除前端模型偏好/能力配置 UI(不再需要用户手动配置) 3. 新增用户-模型维度调用次数计数器 (UserModelUsageCount) - 原子递增,避免从 Usage 表聚合查询 - 前端模型目录和用户可用模型列表展示调用次数 4. 其他改进 - global_model_id 改为必填(NOT NULL),清理孤立模型 - 模型映射对话框支持从上游获取模型列表并分组折叠 - 端点测试不再依赖端点启用状态 - 异步任务页面对普通用户隐藏用户信息列 - Dashboard 响应式布局断点调整 (sm -> lg) - 号池管理仅展示已启用号池的提供商
This commit is contained in:
@@ -231,7 +231,14 @@ class DefaultBillingRuleGenerator:
|
||||
"source": "tiered",
|
||||
"tier_key": tier_key,
|
||||
"allow_zero": True,
|
||||
"tiers": _tiers_for("cache_creation_price_per_1m", default_multiplier=1.25),
|
||||
# TTL override supported when dims include cache_ttl_minutes
|
||||
"ttl_key": "cache_ttl_minutes",
|
||||
"ttl_value_key": "cache_creation_price_per_1m",
|
||||
"tiers": _tiers_for(
|
||||
"cache_creation_price_per_1m",
|
||||
default_multiplier=1.25,
|
||||
include_cache_ttl_pricing=True,
|
||||
),
|
||||
"default": base_cache_creation_price,
|
||||
}
|
||||
dimension_mappings["cache_read_price_per_1m"] = {
|
||||
|
||||
@@ -545,8 +545,8 @@ class GlobalModelService:
|
||||
models_to_delete: list[Model] = []
|
||||
|
||||
for model in models:
|
||||
# 跳过没有关联 GlobalModel 的
|
||||
if not model.global_model_id or not model.global_model:
|
||||
# 跳过 global_model 关系未加载的
|
||||
if not model.global_model:
|
||||
continue
|
||||
|
||||
global_model = cast(GlobalModel, model.global_model)
|
||||
|
||||
@@ -65,15 +65,14 @@ class ModelService:
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
# 显式加载 global_model 关系
|
||||
if model.global_model_id:
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
model = (
|
||||
db.query(Model)
|
||||
.options(joinedload(Model.global_model))
|
||||
.filter(Model.id == model.id)
|
||||
.first()
|
||||
)
|
||||
model = (
|
||||
db.query(Model)
|
||||
.options(joinedload(Model.global_model))
|
||||
.filter(Model.id == model.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"创建模型成功: provider={provider.name}, model={model.provider_model_name}, global_model_id={model.global_model_id}"
|
||||
@@ -226,7 +225,7 @@ class ModelService:
|
||||
)
|
||||
|
||||
# 清除内存缓存(ModelMapperMiddleware 实例)
|
||||
if model.provider_id and model.global_model_id:
|
||||
if model.provider_id:
|
||||
cache_service = get_cache_invalidation_service()
|
||||
cache_service.on_model_changed(model.provider_id, model.global_model_id)
|
||||
|
||||
@@ -297,7 +296,7 @@ class ModelService:
|
||||
)
|
||||
|
||||
# 清除内存缓存
|
||||
if cache_info["provider_id"] and cache_info["global_model_id"]:
|
||||
if cache_info["provider_id"]:
|
||||
cache_service = get_cache_invalidation_service()
|
||||
cache_service.on_model_changed(
|
||||
cache_info["provider_id"], cache_info["global_model_id"]
|
||||
@@ -338,7 +337,7 @@ class ModelService:
|
||||
)
|
||||
|
||||
# 清除内存缓存(ModelMapperMiddleware 实例)
|
||||
if model.provider_id and model.global_model_id:
|
||||
if model.provider_id:
|
||||
cache_service = get_cache_invalidation_service()
|
||||
cache_service.on_model_changed(model.provider_id, model.global_model_id)
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ from sqlalchemy.orm import Session, selectinload
|
||||
from src.core.api_format.conversion.compatibility import is_format_compatible
|
||||
from src.core.api_format.enums import EndpointKind
|
||||
from src.core.api_format.signature import make_signature_key, parse_signature_key
|
||||
from src.core.key_capabilities import check_capability_match
|
||||
from src.core.key_capabilities import (
|
||||
CapabilityMatchMode,
|
||||
check_capability_match,
|
||||
compute_capability_score,
|
||||
get_capability,
|
||||
)
|
||||
from src.core.logger import logger
|
||||
from src.core.model_permissions import check_model_allowed_with_mappings
|
||||
from src.models.database import (
|
||||
@@ -219,9 +224,13 @@ class CandidateBuilder:
|
||||
# 检查模型是否支持所需的能力(在 Provider 级别检查,而不是 Key 级别)
|
||||
# 只有当 model_supported_capabilities 非空时才进行检查
|
||||
# 空列表意味着模型没有配置能力限制,默认支持所有能力
|
||||
# COMPATIBLE 能力跳过模型级硬过滤(交由排序阶段处理)
|
||||
if capability_requirements and model_supported_capabilities:
|
||||
for cap_name, is_required in capability_requirements.items():
|
||||
if is_required and cap_name not in model_supported_capabilities:
|
||||
cap_def = get_capability(cap_name)
|
||||
if cap_def and cap_def.match_mode == CapabilityMatchMode.COMPATIBLE:
|
||||
continue
|
||||
return (
|
||||
False,
|
||||
f"模型 {model_name} 不支持能力: {cap_name}",
|
||||
@@ -617,6 +626,15 @@ class CandidateBuilder:
|
||||
needs_conversion=needs_conversion,
|
||||
provider_api_format=str(endpoint_format_str or ""),
|
||||
output_limit=output_limit,
|
||||
# is_skipped 候选不参与排序,miss_count 无意义,置 0 避免干扰
|
||||
capability_miss_count=(
|
||||
compute_capability_score(
|
||||
key.capabilities or {},
|
||||
capability_requirements,
|
||||
)
|
||||
if is_available
|
||||
else 0
|
||||
),
|
||||
)
|
||||
|
||||
if needs_conversion:
|
||||
|
||||
@@ -18,6 +18,8 @@ from src.services.scheduling.utils import affinity_hash
|
||||
from src.services.system.config import SystemConfigService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.models.database import ProviderAPIKey
|
||||
@@ -30,6 +32,28 @@ class CandidateSorter:
|
||||
def __init__(self, config: SchedulingConfig) -> None:
|
||||
self._config = config
|
||||
|
||||
@staticmethod
|
||||
def _split_by_capability_match(
|
||||
candidates: list[ProviderCandidate],
|
||||
) -> tuple[list[ProviderCandidate], list[ProviderCandidate]]:
|
||||
"""按 capability_miss_count 分组:完全匹配(0)在前,部分匹配(>0)在后"""
|
||||
full_match = [c for c in candidates if c.capability_miss_count == 0]
|
||||
partial_match = [c for c in candidates if c.capability_miss_count > 0]
|
||||
return full_match, partial_match
|
||||
|
||||
def _with_capability_split(
|
||||
self,
|
||||
candidates: list[ProviderCandidate],
|
||||
sort_fn: Callable[..., list[ProviderCandidate]],
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> list[ProviderCandidate]:
|
||||
"""通用包装:先按 capability_miss_count 分组,再分别排序后合并"""
|
||||
if not candidates:
|
||||
return candidates
|
||||
full_match, partial_match = self._split_by_capability_match(candidates)
|
||||
return sort_fn(full_match, *args, **kwargs) + sort_fn(partial_match, *args, **kwargs)
|
||||
|
||||
def _apply_priority_mode_sort(
|
||||
self,
|
||||
candidates: list[ProviderCandidate],
|
||||
@@ -40,7 +64,8 @@ class CandidateSorter:
|
||||
"""
|
||||
根据优先级模式对候选列表排序(数字越小越优先)
|
||||
|
||||
排序规则(受 keep_priority_on_conversion 配置影响):
|
||||
排序规则:
|
||||
0. 按 capability_miss_count 分组:完全匹配(0)在前,部分匹配(>0)在后
|
||||
1. 如果全局配置 keep_priority_on_conversion=True,所有候选保持原优先级
|
||||
2. 否则,按 needs_conversion 和 provider.keep_priority_on_conversion 分组:
|
||||
- 保持优先级的候选(exact 或 provider.keep_priority_on_conversion=True)按原优先级排序
|
||||
@@ -52,6 +77,21 @@ class CandidateSorter:
|
||||
if not candidates:
|
||||
return candidates
|
||||
|
||||
return self._with_capability_split(
|
||||
candidates, self._apply_priority_mode_sort_inner, db, affinity_key, api_format
|
||||
)
|
||||
|
||||
def _apply_priority_mode_sort_inner(
|
||||
self,
|
||||
candidates: list[ProviderCandidate],
|
||||
db: Session,
|
||||
affinity_key: str | None = None,
|
||||
api_format: str | None = None,
|
||||
) -> list[ProviderCandidate]:
|
||||
"""优先级模式排序的内部实现(不含 capability_miss_count 分组)"""
|
||||
if not candidates:
|
||||
return candidates
|
||||
|
||||
# 全局配置:如果开启,所有候选保持原优先级
|
||||
global_keep_priority = SystemConfigService.is_keep_priority_on_conversion(db)
|
||||
|
||||
@@ -159,6 +199,7 @@ class CandidateSorter:
|
||||
负载均衡模式:同优先级内随机轮换
|
||||
|
||||
排序逻辑:
|
||||
0. 按 capability_miss_count 分组:完全匹配(0)在前,部分匹配(>0)在后
|
||||
1. 按优先级分组(provider_priority, internal_priority 或 global_priority_by_format)
|
||||
2. 同优先级组内随机打乱
|
||||
3. 不考虑缓存亲和性
|
||||
@@ -166,6 +207,15 @@ class CandidateSorter:
|
||||
if not candidates:
|
||||
return candidates
|
||||
|
||||
return self._with_capability_split(candidates, self._apply_load_balance_inner, api_format)
|
||||
|
||||
def _apply_load_balance_inner(
|
||||
self, candidates: list[ProviderCandidate], api_format: str | None = None
|
||||
) -> list[ProviderCandidate]:
|
||||
"""负载均衡排序的内部实现(不含 capability_miss_count 分组)"""
|
||||
if not candidates:
|
||||
return candidates
|
||||
|
||||
priority_groups: dict[tuple, list[ProviderCandidate]] = defaultdict(list)
|
||||
|
||||
# 根据优先级模式选择分组方式
|
||||
|
||||
@@ -29,6 +29,7 @@ class ProviderCandidate:
|
||||
needs_conversion: bool = False # 是否需要格式转换
|
||||
provider_api_format: str = "" # Provider 端点实际格式(用于健康度/熔断 bucket)
|
||||
output_limit: int | None = None # GlobalModel 配置的模型输出上限
|
||||
capability_miss_count: int = 0 # COMPATIBLE 能力不匹配数(0=完全匹配,用于排序)
|
||||
|
||||
def _stable_order_key(self) -> tuple[int, int, str, str, str]:
|
||||
"""
|
||||
|
||||
@@ -133,6 +133,8 @@ class UsageBillingIntegrationMixin:
|
||||
output_tokens=params.output_tokens,
|
||||
cache_creation_input_tokens=params.cache_creation_input_tokens,
|
||||
cache_read_input_tokens=params.cache_read_input_tokens,
|
||||
cache_creation_input_tokens_5m=params.cache_creation_input_tokens_5m,
|
||||
cache_creation_input_tokens_1h=params.cache_creation_input_tokens_1h,
|
||||
request_type=params.request_type,
|
||||
api_format=params.api_format,
|
||||
api_family=params.api_family,
|
||||
|
||||
@@ -55,6 +55,8 @@ def build_usage_params(
|
||||
output_tokens: int,
|
||||
cache_creation_input_tokens: int,
|
||||
cache_read_input_tokens: int,
|
||||
cache_creation_input_tokens_5m: int = 0,
|
||||
cache_creation_input_tokens_1h: int = 0,
|
||||
request_type: str,
|
||||
api_format: str | None,
|
||||
api_family: str | None = None,
|
||||
@@ -201,6 +203,8 @@ def build_usage_params(
|
||||
"total_tokens": input_tokens + output_tokens,
|
||||
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||
"cache_read_input_tokens": cache_read_input_tokens,
|
||||
"cache_creation_input_tokens_5m": cache_creation_input_tokens_5m,
|
||||
"cache_creation_input_tokens_1h": cache_creation_input_tokens_1h,
|
||||
"input_cost_usd": input_cost,
|
||||
"output_cost_usd": output_cost,
|
||||
"cache_cost_usd": cache_cost,
|
||||
@@ -292,6 +296,12 @@ def update_existing_usage(
|
||||
existing_usage.total_tokens = usage_params["total_tokens"]
|
||||
existing_usage.cache_creation_input_tokens = usage_params["cache_creation_input_tokens"]
|
||||
existing_usage.cache_read_input_tokens = usage_params["cache_read_input_tokens"]
|
||||
existing_usage.cache_creation_input_tokens_5m = usage_params.get(
|
||||
"cache_creation_input_tokens_5m", 0
|
||||
)
|
||||
existing_usage.cache_creation_input_tokens_1h = usage_params.get(
|
||||
"cache_creation_input_tokens_1h", 0
|
||||
)
|
||||
existing_usage.input_cost_usd = usage_params["input_cost_usd"]
|
||||
existing_usage.output_cost_usd = usage_params["output_cost_usd"]
|
||||
existing_usage.cache_cost_usd = usage_params["cache_cost_usd"]
|
||||
|
||||
@@ -49,6 +49,8 @@ class UsageRecordParams:
|
||||
cache_ttl_minutes: int | None
|
||||
use_tiered_pricing: bool
|
||||
target_model: str | None
|
||||
cache_creation_input_tokens_5m: int = 0
|
||||
cache_creation_input_tokens_1h: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""验证关键字段,确保数据完整性"""
|
||||
|
||||
@@ -68,6 +68,8 @@ def _event_to_record(event: UsageEvent) -> dict[str, Any]:
|
||||
"output_tokens": data.get("output_tokens") or 0,
|
||||
"cache_creation_input_tokens": data.get("cache_creation_input_tokens") or 0,
|
||||
"cache_read_input_tokens": data.get("cache_read_input_tokens") or 0,
|
||||
"cache_creation_input_tokens_5m": data.get("cache_creation_input_tokens_5m") or 0,
|
||||
"cache_creation_input_tokens_1h": data.get("cache_creation_input_tokens_1h") or 0,
|
||||
"request_type": data.get("request_type") or "chat",
|
||||
"api_format": data.get("api_format"),
|
||||
"api_family": data.get("api_family"),
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.logger import logger
|
||||
from src.models.database import ApiKey, Provider, Usage, User
|
||||
from src.models.database import ApiKey, Provider, Usage, User, UserModelUsageCount
|
||||
from src.services.usage._billing_integration import UsageBillingIntegrationMixin
|
||||
from src.services.usage._recording_helpers import (
|
||||
METADATA_KEEP_KEYS,
|
||||
@@ -44,6 +44,31 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
"""更新已存在的 Usage 记录(委托到模块级函数)"""
|
||||
update_existing_usage(existing_usage, usage_params, target_model)
|
||||
|
||||
@staticmethod
|
||||
def _increment_user_model_usage(
|
||||
db: Session, user: User | None, model: str, count: int = 1
|
||||
) -> None:
|
||||
"""原子递增用户-模型调用次数计数器"""
|
||||
if user is None:
|
||||
return
|
||||
from sqlalchemy import func as sa_func
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
stmt = pg_insert(UserModelUsageCount).values(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user.id,
|
||||
model=model,
|
||||
usage_count=count,
|
||||
)
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
constraint="uq_user_model_usage_count",
|
||||
set_={
|
||||
"usage_count": UserModelUsageCount.usage_count + count,
|
||||
"updated_at": sa_func.now(),
|
||||
},
|
||||
)
|
||||
db.execute(stmt)
|
||||
|
||||
@classmethod
|
||||
def _sanitize_request_metadata(cls, metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
"""元数据清理(委托到模块级函数)"""
|
||||
@@ -65,6 +90,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens: int,
|
||||
cache_creation_input_tokens: int = 0,
|
||||
cache_read_input_tokens: int = 0,
|
||||
cache_creation_input_tokens_5m: int = 0,
|
||||
cache_creation_input_tokens_1h: int = 0,
|
||||
request_type: str = "chat",
|
||||
api_format: str | None = None,
|
||||
api_family: str | None = None,
|
||||
@@ -116,6 +143,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens=output_tokens,
|
||||
cache_creation_input_tokens=cache_creation_input_tokens,
|
||||
cache_read_input_tokens=cache_read_input_tokens,
|
||||
cache_creation_input_tokens_5m=cache_creation_input_tokens_5m,
|
||||
cache_creation_input_tokens_1h=cache_creation_input_tokens_1h,
|
||||
request_type=request_type,
|
||||
api_format=api_format,
|
||||
api_family=api_family,
|
||||
@@ -162,6 +191,9 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
.values(usage_count=GlobalModel.usage_count + 1)
|
||||
)
|
||||
|
||||
# 更新用户-模型调用次数计数器
|
||||
cls._increment_user_model_usage(db, user, model)
|
||||
|
||||
# 更新 Provider 月度使用量(原子操作)
|
||||
if provider_id:
|
||||
actual_total_cost = usage_params["actual_total_cost_usd"]
|
||||
@@ -191,6 +223,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens: int,
|
||||
cache_creation_input_tokens: int = 0,
|
||||
cache_read_input_tokens: int = 0,
|
||||
cache_creation_input_tokens_5m: int = 0,
|
||||
cache_creation_input_tokens_1h: int = 0,
|
||||
request_type: str = "chat",
|
||||
api_format: str | None = None,
|
||||
api_family: str | None = None,
|
||||
@@ -244,6 +278,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens=output_tokens,
|
||||
cache_creation_input_tokens=cache_creation_input_tokens,
|
||||
cache_read_input_tokens=cache_read_input_tokens,
|
||||
cache_creation_input_tokens_5m=cache_creation_input_tokens_5m,
|
||||
cache_creation_input_tokens_1h=cache_creation_input_tokens_1h,
|
||||
request_type=request_type,
|
||||
api_format=api_format,
|
||||
api_family=api_family,
|
||||
@@ -347,6 +383,9 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
.values(usage_count=GlobalModel.usage_count + 1)
|
||||
)
|
||||
|
||||
# 更新用户-模型调用次数计数器
|
||||
cls._increment_user_model_usage(db, user, model)
|
||||
|
||||
# 更新 Provider 月度使用量
|
||||
if provider_id:
|
||||
actual_total_cost = usage_params["actual_total_cost_usd"]
|
||||
@@ -387,6 +426,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens: int = 0,
|
||||
cache_creation_input_tokens: int = 0,
|
||||
cache_read_input_tokens: int = 0,
|
||||
cache_creation_input_tokens_5m: int = 0,
|
||||
cache_creation_input_tokens_1h: int = 0,
|
||||
api_format: str | None = None,
|
||||
api_family: str | None = None,
|
||||
endpoint_kind: str | None = None,
|
||||
@@ -451,6 +492,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens=output_tokens,
|
||||
cache_creation_input_tokens=cache_creation_input_tokens,
|
||||
cache_read_input_tokens=cache_read_input_tokens,
|
||||
cache_creation_input_tokens_5m=cache_creation_input_tokens_5m,
|
||||
cache_creation_input_tokens_1h=cache_creation_input_tokens_1h,
|
||||
request_type=request_type,
|
||||
api_format=api_format,
|
||||
api_family=api_family,
|
||||
@@ -588,6 +631,9 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
.values(usage_count=GlobalModel.usage_count + 1)
|
||||
)
|
||||
|
||||
# 更新用户-模型调用次数计数器
|
||||
cls._increment_user_model_usage(db, user, model)
|
||||
|
||||
# 更新 Provider 月度使用量(使用 actual_total_cost)
|
||||
if provider_id:
|
||||
actual_total_cost = usage_params["actual_total_cost_usd"]
|
||||
@@ -714,6 +760,9 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
lambda: {"requests": 0, "cost": 0.0, "is_standalone": False}
|
||||
)
|
||||
model_counts: dict[str, int] = defaultdict(int) # model -> count
|
||||
user_model_counts: dict[tuple[str, str], int] = defaultdict(
|
||||
int
|
||||
) # (user_id, model) -> count
|
||||
provider_costs: dict[str, float] = defaultdict(float) # provider_id -> cost
|
||||
|
||||
# 合并所有需要处理的记录(用于预取 user/api_key)
|
||||
@@ -754,6 +803,12 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
output_tokens=int(record.get("output_tokens") or 0),
|
||||
cache_creation_input_tokens=int(record.get("cache_creation_input_tokens") or 0),
|
||||
cache_read_input_tokens=int(record.get("cache_read_input_tokens") or 0),
|
||||
cache_creation_input_tokens_5m=int(
|
||||
record.get("cache_creation_input_tokens_5m") or 0
|
||||
),
|
||||
cache_creation_input_tokens_1h=int(
|
||||
record.get("cache_creation_input_tokens_1h") or 0
|
||||
),
|
||||
request_type=record.get("request_type") or "chat",
|
||||
api_format=record.get("api_format"),
|
||||
api_family=record.get("api_family"),
|
||||
@@ -850,6 +905,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
# 聚合统计
|
||||
model_name = record.get("model") or "unknown"
|
||||
model_counts[model_name] += 1
|
||||
if user:
|
||||
user_model_counts[(str(user.id), model_name)] += 1
|
||||
|
||||
provider_id = record.get("provider_id")
|
||||
if provider_id:
|
||||
@@ -898,6 +955,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
# 聚合统计
|
||||
model_name = record.get("model") or "unknown"
|
||||
model_counts[model_name] += 1
|
||||
if user:
|
||||
user_model_counts[(str(user.id), model_name)] += 1
|
||||
|
||||
provider_id = record.get("provider_id")
|
||||
if provider_id:
|
||||
@@ -959,6 +1018,30 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
.values(usage_count=GlobalModel.usage_count + count)
|
||||
)
|
||||
|
||||
# 批量更新用户-模型调用次数计数器
|
||||
from sqlalchemy import func as sql_func
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
if user_model_counts:
|
||||
rows = [
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_id": uid,
|
||||
"model": model_name,
|
||||
"usage_count": count,
|
||||
}
|
||||
for (uid, model_name), count in user_model_counts.items()
|
||||
]
|
||||
stmt = pg_insert(UserModelUsageCount).values(rows)
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
constraint="uq_user_model_usage_count",
|
||||
set_={
|
||||
"usage_count": UserModelUsageCount.usage_count + stmt.excluded.usage_count,
|
||||
"updated_at": sql_func.now(),
|
||||
},
|
||||
)
|
||||
db.execute(stmt)
|
||||
|
||||
# 批量更新 Provider 月度使用量
|
||||
for provider_id, cost in provider_costs.items():
|
||||
if cost > 0:
|
||||
@@ -969,8 +1052,6 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
|
||||
)
|
||||
|
||||
# 批量更新用户使用量
|
||||
from sqlalchemy import func as sql_func
|
||||
|
||||
for user_id, cost in user_costs.items():
|
||||
if cost > 0:
|
||||
db.execute(
|
||||
|
||||
@@ -100,6 +100,8 @@ class StreamUsageTracker:
|
||||
self.output_tokens = 0
|
||||
self.cache_creation_input_tokens = 0
|
||||
self.cache_read_input_tokens = 0
|
||||
self.cache_creation_input_tokens_5m = 0
|
||||
self.cache_creation_input_tokens_1h = 0
|
||||
self.accumulated_content = ""
|
||||
|
||||
# 完整响应跟踪(仅用于内部统计,不记录到数据库)
|
||||
@@ -477,6 +479,8 @@ class StreamUsageTracker:
|
||||
"""
|
||||
import time
|
||||
|
||||
from src.api.handlers.base.utils import extract_cache_creation_tokens_detail
|
||||
|
||||
self.start_time = time.time()
|
||||
self.request_data = request_data # 保存请求数据
|
||||
|
||||
@@ -545,21 +549,17 @@ class StreamUsageTracker:
|
||||
# 如果响应中包含准确的usage信息,使用它
|
||||
self.input_tokens = usage.get("input_tokens", self.input_tokens)
|
||||
self.output_tokens = usage.get("output_tokens", self.output_tokens)
|
||||
self.cache_creation_input_tokens = usage.get(
|
||||
"cache_creation_input_tokens", self.cache_creation_input_tokens
|
||||
)
|
||||
self.cache_read_input_tokens = usage.get(
|
||||
"cache_read_input_tokens", self.cache_read_input_tokens
|
||||
)
|
||||
|
||||
# 处理新的cache_creation格式
|
||||
if "cache_creation" in usage:
|
||||
cache_creation_data = usage.get("cache_creation", {})
|
||||
# 如果没有cache_creation_input_tokens,尝试从cache_creation中获取
|
||||
if not self.cache_creation_input_tokens:
|
||||
self.cache_creation_input_tokens = cache_creation_data.get(
|
||||
"ephemeral_5m_input_tokens", 0
|
||||
) + cache_creation_data.get("ephemeral_1h_input_tokens", 0)
|
||||
# 统一提取 cache_creation tokens(新格式优先于旧格式)
|
||||
total, t5m, t1h = extract_cache_creation_tokens_detail(usage)
|
||||
if total:
|
||||
self.cache_creation_input_tokens = total
|
||||
if t5m or t1h:
|
||||
self.cache_creation_input_tokens_5m = t5m
|
||||
self.cache_creation_input_tokens_1h = t1h
|
||||
|
||||
finally:
|
||||
# 流结束后记录使用量
|
||||
@@ -768,6 +768,8 @@ class StreamUsageTracker:
|
||||
output_tokens=self.output_tokens,
|
||||
cache_creation_input_tokens=self.cache_creation_input_tokens,
|
||||
cache_read_input_tokens=self.cache_read_input_tokens,
|
||||
cache_creation_input_tokens_5m=self.cache_creation_input_tokens_5m,
|
||||
cache_creation_input_tokens_1h=self.cache_creation_input_tokens_1h,
|
||||
request_type="chat",
|
||||
api_format=self.api_format,
|
||||
api_family=self.api_family,
|
||||
|
||||
@@ -70,6 +70,8 @@ class MessageTelemetry:
|
||||
client_response_headers: dict[str, Any] | None = None,
|
||||
cache_creation_tokens: int = 0,
|
||||
cache_read_tokens: int = 0,
|
||||
cache_creation_tokens_5m: int = 0,
|
||||
cache_creation_tokens_1h: int = 0,
|
||||
is_stream: bool = False,
|
||||
provider_request_headers: dict[str, Any] | None = None,
|
||||
provider_request_body: Any | None = None,
|
||||
@@ -111,6 +113,8 @@ class MessageTelemetry:
|
||||
output_tokens=output_tokens,
|
||||
cache_creation_input_tokens=cache_creation_tokens,
|
||||
cache_read_input_tokens=cache_read_tokens,
|
||||
cache_creation_input_tokens_5m=cache_creation_tokens_5m,
|
||||
cache_creation_input_tokens_1h=cache_creation_tokens_1h,
|
||||
request_type="chat",
|
||||
api_format=api_format,
|
||||
api_family=api_family,
|
||||
@@ -181,6 +185,8 @@ class MessageTelemetry:
|
||||
output_tokens: int = 0,
|
||||
cache_creation_tokens: int = 0,
|
||||
cache_read_tokens: int = 0,
|
||||
cache_creation_tokens_5m: int = 0,
|
||||
cache_creation_tokens_1h: int = 0,
|
||||
response_body: dict[str, Any] | None = None,
|
||||
response_headers: dict[str, Any] | None = None,
|
||||
client_response_headers: dict[str, Any] | None = None,
|
||||
@@ -227,6 +233,8 @@ class MessageTelemetry:
|
||||
output_tokens=output_tokens,
|
||||
cache_creation_input_tokens=cache_creation_tokens,
|
||||
cache_read_input_tokens=cache_read_tokens,
|
||||
cache_creation_input_tokens_5m=cache_creation_tokens_5m,
|
||||
cache_creation_input_tokens_1h=cache_creation_tokens_1h,
|
||||
request_type="chat",
|
||||
api_format=api_format,
|
||||
api_family=api_family,
|
||||
@@ -276,6 +284,8 @@ class MessageTelemetry:
|
||||
output_tokens: int = 0,
|
||||
cache_creation_tokens: int = 0,
|
||||
cache_read_tokens: int = 0,
|
||||
cache_creation_tokens_5m: int = 0,
|
||||
cache_creation_tokens_1h: int = 0,
|
||||
response_body: dict[str, Any] | None = None,
|
||||
response_headers: dict[str, Any] | None = None,
|
||||
client_response_headers: dict[str, Any] | None = None,
|
||||
@@ -308,6 +318,8 @@ class MessageTelemetry:
|
||||
output_tokens=output_tokens,
|
||||
cache_creation_input_tokens=cache_creation_tokens,
|
||||
cache_read_input_tokens=cache_read_tokens,
|
||||
cache_creation_input_tokens_5m=cache_creation_tokens_5m,
|
||||
cache_creation_input_tokens_1h=cache_creation_tokens_1h,
|
||||
request_type="chat",
|
||||
api_format=api_format,
|
||||
api_family=api_family,
|
||||
|
||||
@@ -206,6 +206,14 @@ class QueueTelemetryWriter(TelemetryWriter):
|
||||
if cache_read:
|
||||
data["cache_read_input_tokens"] = cache_read
|
||||
|
||||
# 缓存 5m/1h 细分
|
||||
cache_creation_5m = kwargs.get("cache_creation_tokens_5m", 0)
|
||||
cache_creation_1h = kwargs.get("cache_creation_tokens_1h", 0)
|
||||
if cache_creation_5m:
|
||||
data["cache_creation_input_tokens_5m"] = cache_creation_5m
|
||||
if cache_creation_1h:
|
||||
data["cache_creation_input_tokens_1h"] = cache_creation_1h
|
||||
|
||||
# 时间指标
|
||||
if kwargs.get("response_time_ms") is not None:
|
||||
data["response_time_ms"] = kwargs["response_time_ms"]
|
||||
|
||||
Reference in New Issue
Block a user