fix: stop passing est_tokens off as measured usage
usage 帧缺失/非法时不再拿 est_tokens(最坏情形上界)当实测值,chat 与 embedding 两处兜底改记 0 并标 unavailable;打捞覆盖加 measured 前置条件, 避免 0/0 被洗成 estimated 而算出假的 0.0。embedding 全批合并扩三态(任一批 不可得 → 整体不可得),_total_cost 遇不可得批整体记 NULL。
This commit is contained in:
@@ -385,14 +385,21 @@ class EmbeddingClient:
|
||||
vectors = [_l2_normalize(v) for v in vectors]
|
||||
first = outcomes[0]
|
||||
prompt_tokens = sum(o.result.prompt_tokens for o in outcomes)
|
||||
estimated = any(o.result.usage_source == "estimated" for o in outcomes)
|
||||
# 三态合并优先级(解耦设计 §3.2 #10): 任一批不可得 → 整体不可得
|
||||
sources = {o.result.usage_source for o in outcomes}
|
||||
if "unavailable" in sources:
|
||||
merged_source = "unavailable"
|
||||
elif "estimated" in sources:
|
||||
merged_source = "estimated"
|
||||
else:
|
||||
merged_source = "measured"
|
||||
return EmbeddingResponse(
|
||||
vectors=vectors,
|
||||
dim=first.result.dim,
|
||||
model=first.source.model,
|
||||
provider=first.source.provider,
|
||||
prompt_tokens=prompt_tokens,
|
||||
usage_source="estimated" if estimated else "measured",
|
||||
usage_source=merged_source,
|
||||
latency_ms=sum(o.latency_ms for o in outcomes),
|
||||
call_id=first.call_id,
|
||||
source_name=first.source.name,
|
||||
@@ -400,8 +407,15 @@ class EmbeddingClient:
|
||||
)
|
||||
|
||||
def _total_cost(self, outcomes: list[_BatchOutcome]) -> float | None:
|
||||
"""全批成本;任一批用量不可得则整体记 NULL(解耦设计 §3.2 #11)。
|
||||
|
||||
逐批求和会把不可得的批当 0 计入,给出一个偏低却看似有效的金额——
|
||||
与"宁可算不出成本,也不算错成本"的不变式相悖。
|
||||
"""
|
||||
if self._pricing is None:
|
||||
return None
|
||||
if any(o.result.usage_source == "unavailable" for o in outcomes):
|
||||
return None
|
||||
costs = [self._pricing.cost(o.source.model, o.result.prompt_tokens, 0) for o in outcomes]
|
||||
known = [c for c in costs if c is not None]
|
||||
return sum(known) if known else None
|
||||
|
||||
@@ -139,11 +139,31 @@ def _strip_think(content: str) -> tuple[str, str]:
|
||||
|
||||
|
||||
def _resolve_usage(usage: dict[str, Any], source: SourceConfig) -> tuple[int, int, str]:
|
||||
"""usage 帧读取;缺失/非法按 est_tokens 保守兜底并标 estimated(CHS invokers.py:241)。"""
|
||||
"""usage 帧读取;缺失/非法记 0/0 并标 unavailable(est_tokens 解耦设计 §3.2 #3)。
|
||||
|
||||
不再拿 `est_tokens` 兜底: 它按 CHS 定义是"最坏情形上界",拿上界当实测值
|
||||
只会系统性高估账单;宁可把用量记成显式的"不可得"(cost 随之为 NULL),
|
||||
让缺口可被统计,也不编一个看似有效的数字。
|
||||
"""
|
||||
prompt, completion = usage.get("prompt_tokens"), usage.get("completion_tokens")
|
||||
if isinstance(prompt, int) and isinstance(completion, int) and prompt + completion > 0:
|
||||
return prompt, completion, "measured"
|
||||
return 0, source.est_tokens, "estimated"
|
||||
return 0, 0, "unavailable"
|
||||
|
||||
|
||||
def _resolve_stream_usage(
|
||||
sink: dict[str, Any], salvaged: bool, source: SourceConfig
|
||||
) -> tuple[int, int, str]:
|
||||
"""流式用量口径: 打捞路径把 measured 降级为 estimated,unavailable 原样保留。
|
||||
|
||||
前置条件不可省(解耦设计 §3.2 #4): usage 帧本就缺失时 `0/0` 会被洗成
|
||||
`estimated`,进而按 token 换算出一个假的 `0.0` 成本。
|
||||
"""
|
||||
prompt, completion, usage_source = _resolve_usage(sink.get("usage") or {}, source)
|
||||
if salvaged and usage_source == "measured":
|
||||
# 收到 usage 帧但流被截断: 数字真实、可信度降级(M1 设计 §6)
|
||||
usage_source = "estimated"
|
||||
return prompt, completion, usage_source
|
||||
|
||||
|
||||
def _extract_vectors(
|
||||
@@ -169,11 +189,11 @@ def _extract_vectors(
|
||||
|
||||
|
||||
def _resolve_embedding_usage(data: dict[str, Any], source: SourceConfig) -> tuple[int, str]:
|
||||
"""usage 读取;缺失/非法按 est_tokens 保守兜底并标 estimated(与 chat 同口径)。"""
|
||||
"""usage 读取;缺失/非法记 0 并标 unavailable(与 chat 同口径,设计 §3.2 #3)。"""
|
||||
prompt = (data.get("usage") or {}).get("prompt_tokens")
|
||||
if isinstance(prompt, int) and prompt > 0:
|
||||
return prompt, "measured"
|
||||
return source.est_tokens, "estimated"
|
||||
return 0, "unavailable"
|
||||
|
||||
|
||||
def _parse_embedding_payload(
|
||||
@@ -331,9 +351,7 @@ class OpenAICompatTransport:
|
||||
salvaged = self._check_done(sink, content_parts, thinking_parts, source)
|
||||
content, thinking = self._finalize_text(content_parts, thinking_parts, profile)
|
||||
self._reject_empty_completion(content, source)
|
||||
prompt, completion, usage_source = _resolve_usage(sink.get("usage") or {}, source)
|
||||
if salvaged:
|
||||
usage_source = "estimated" # 打捞路径强制 estimated(设计 §6)
|
||||
prompt, completion, usage_source = _resolve_stream_usage(sink, salvaged, source)
|
||||
return TransportResult(
|
||||
content=content,
|
||||
thinking=thinking,
|
||||
|
||||
@@ -284,7 +284,7 @@ class EmbeddingTransportResult:
|
||||
vectors: list[list[float]]
|
||||
dim: int
|
||||
prompt_tokens: int
|
||||
usage_source: str # measured | estimated
|
||||
usage_source: str # measured | estimated | unavailable
|
||||
raw: dict[str, Any]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user