fix: 统一 input_context_expr 计算口径,移除按 api_format 分支的 CASE 逻辑

input_context_expr() 原先按 OpenAI/Gemini 和 Claude 分支计算输入上下文,
现统一为 input_tokens + cache_read_input_tokens,与 usage 表展示口径一致。
同步更新 admin 和 user_me 路由中的缓存命中率注释,并新增单元测试。
This commit is contained in:
fawney19
2026-03-19 01:35:00 +08:00
parent 8a6a961900
commit 56f3c95763
4 changed files with 91 additions and 23 deletions

View File

@@ -87,8 +87,8 @@ def _calculate_token_cache_hit_rate(
Args:
total_input_context: 已归一化的总输入上下文 token 数。
Claude 格式: input_tokens + cache_read_input_tokens
OpenAI/Gemini 格式: input_tokens(已包含 cache_read
由 query.py 的 input_context_expr() 统一计算,固定为
input_tokens + cache_read_input_tokens。
cache_read_tokens: 缓存读取 token 数
"""
context = int(total_input_context or 0)
@@ -1961,9 +1961,7 @@ def _apply_replay_model_to_body(
"""根据目标格式决定是否写入 body.model。"""
from src.core.api_format.metadata import resolve_endpoint_definition
target_meta = (
resolve_endpoint_definition(target_api_format) if target_api_format else None
)
target_meta = resolve_endpoint_definition(target_api_format) if target_api_format else None
if target_meta is not None and not target_meta.model_in_body:
body.pop("model", None)
return
@@ -1997,9 +1995,7 @@ class AdminUsageReplayAdapter(AdminApiAdapter):
original_api_format.split(":")[0] if ":" in original_api_format else ""
)
original_request_body = usage_record.get_request_body()
body_override_payload = (
self.body_override if isinstance(self.body_override, dict) else None
)
body_override_payload = self.body_override if isinstance(self.body_override, dict) else None
override_model: str | None = None
if isinstance(body_override_payload, dict):
override_val = body_override_payload.get("model")
@@ -2262,7 +2258,9 @@ class AdminUsageReplayAdapter(AdminApiAdapter):
# envelope 失败仍发送原始体
# 获取提供商名称
provider_name = target_provider_obj.name if target_provider_obj else usage_record.provider_name
provider_name = (
target_provider_obj.name if target_provider_obj else usage_record.provider_name
)
# 发送请求
try:

View File

@@ -68,7 +68,8 @@ def _calculate_token_cache_hit_rate(total_input_context: int, cache_read_tokens:
"""计算缓存命中率。
Args:
total_input_context: 已归一化的总输入上下文 token 数(由 query.py 按 API 格式精确计算)。
total_input_context: 已归一化的总输入上下文 token 数
(由 query.py 的 input_context_expr() 统一计算,为 input + cache_read
cache_read_tokens: 缓存读取 token 数。
"""
context = max(0, int(total_input_context))

View File

@@ -2,28 +2,25 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any
from typing import TYPE_CHECKING, Any
from sqlalchemy import Case, case, func
from sqlalchemy import case, func
from sqlalchemy.orm import Session
if TYPE_CHECKING:
from sqlalchemy.sql.elements import ColumnElement
from src.core.logger import logger
from src.models.database import ApiKey, Usage, User
def input_context_expr() -> Case:
"""构造 SQL CASE 表达式,根据 api_format 精确计算每条记录的总输入上下文 token 数。
def input_context_expr() -> ColumnElement[int]:
"""计算缓存命中率口径下的总输入上下文 token 数。
- OpenAI/Gemini: input_tokens 已包含 cache_read_input_tokens直接使用
- Claude/未知: input_tokens 不含 cache_read,需要加上
为了与 usage 表中“输入 tokens + 缓存读取 tokens”的展示口径保持一致
聚合统计统一使用 `input_tokens + cache_read_input_tokens` 作为分母。
"""
return case(
(
Usage.api_format.like("openai:%") | Usage.api_format.like("gemini:%"),
Usage.input_tokens,
),
else_=Usage.input_tokens + Usage.cache_read_input_tokens,
)
return Usage.input_tokens + Usage.cache_read_input_tokens
@dataclass(slots=True)