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:
fawney19
2026-02-28 11:44:08 +08:00
parent 82bbed2720
commit ecb16d345a
59 changed files with 1053 additions and 608 deletions

View File

@@ -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"] = {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)
# 根据优先级模式选择分组方式

View File

@@ -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]:
"""

View File

@@ -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,

View File

@@ -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"]

View File

@@ -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:
"""验证关键字段,确保数据完整性"""

View File

@@ -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"),

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"]