fix: settle cancelled attempts against the source estimate
取消发生在"端口已开始、结算尚未确定"时,原先按 settle(0) 把入场预扣整笔 退还,等于把可能已被上游计费的用量退回闸里;启用调用期限后库自身会常规性 触发该路径,故先修记账再启用。 改动只落在取消路径的取值上(设计 §6.3 矩阵 S3/S5/S7): - actual 初值保持 0,另设函数内局部阶段变量 settlement_known(不进任何签名); - 成功路径算出用量后置位,真实 usage 恰为 0 同样算"已知",不被取消覆写; - 已处理领域失败分支把结算决定前移到其第一个 await 之前(同级 except CancelledError 接不住本块 await 上的取消,它直穿 finally), 故 SourceDead 的既有 0 在取消下被保住,瞬时失败仍是 est,值与 1.3.5 逐字相同; - 未被四分类接住的异常不经上述分支,仍按 0 退全款(S8,本版不扩大语义); - OCR 的 0 token 是事实而非未知,settle(0) 不变,只补注释。 取消窗口一律用真实 asyncio.Event 钉死(不再 sleep 撞窗口),并加防越界回归: RuntimeError 逃逸仍结 0、真实 usage 为 0 的成功仍结 0。
This commit is contained in:
@@ -343,6 +343,9 @@ class EmbeddingClient:
|
||||
call_id = str(uuid.uuid4())
|
||||
started = self._now()
|
||||
actual = 0
|
||||
# 局部阶段变量(与 RetryMW 同口径): 该刻库是否已算出确定结算。只服务于取消
|
||||
# 分支的兜底取值,不进任何签名; 未分类异常逃逸时仍逐字走旧的全额退还。
|
||||
settlement_known = False
|
||||
# 登记在 transport 调用**之前**(同 RetryMW): 失败与取消的尝试也真的发出去了
|
||||
context.register_attempt()
|
||||
try:
|
||||
@@ -358,6 +361,8 @@ class EmbeddingClient:
|
||||
actual = source.effective_est_tokens()
|
||||
else:
|
||||
actual = result.prompt_tokens
|
||||
# 真实 usage 恰为 0 也是已知事实, 后续取消不得改写成 est
|
||||
settlement_known = True
|
||||
await self._record_quietly(self._breaker.record_success(entry))
|
||||
await self._record_quietly(self._quota.mark_progress())
|
||||
latency_ms = int((self._now() - started) * 1000)
|
||||
@@ -375,6 +380,7 @@ class EmbeddingClient:
|
||||
)
|
||||
return _BatchOutcome(result, source, call_id, latency_ms)
|
||||
except (RequestRejectedError, ResultInvalidError) as exc:
|
||||
actual, settlement_known = 0, True # 逐字保住 1.3.5 口径(本版不改这一族记账)
|
||||
await self._gate_on_terminal(exc, entry)
|
||||
await self._emit(
|
||||
batch,
|
||||
@@ -390,6 +396,9 @@ class EmbeddingClient:
|
||||
)
|
||||
raise
|
||||
except asyncio.CancelledError:
|
||||
if not settlement_known:
|
||||
# 端口已开始、结算未定: 保守保留预扣(设计 §6.3 S7)
|
||||
actual = source.effective_est_tokens()
|
||||
if entry.is_probe:
|
||||
await self._record_quietly(self._breaker.release_probe(entry))
|
||||
await self._emit(
|
||||
@@ -409,10 +418,11 @@ class EmbeddingClient:
|
||||
dead = isinstance(exc, SourceDeadError)
|
||||
reason = _failure_reason(exc)
|
||||
reasons[source.name] = reason
|
||||
# 结算决定定死在本分支第一个 await 之前: 同级 except CancelledError 接不住
|
||||
# 落在本块 await 上的取消,它直穿 finally。值与 1.3.5 逐字相同,只是算得更早。
|
||||
actual = 0 if dead else source.effective_est_tokens()
|
||||
settlement_known = True
|
||||
await self._record_quietly(self._breaker.record_failure(entry, reason, dead))
|
||||
if not dead:
|
||||
# 保守: 失败请求可能已被网关计费(CHS 同款);与入场预扣同源取值
|
||||
actual = source.effective_est_tokens()
|
||||
await self._emit(
|
||||
batch,
|
||||
source,
|
||||
|
||||
@@ -279,6 +279,9 @@ class RetryMW:
|
||||
call_id = str(uuid.uuid4())
|
||||
started = self._now()
|
||||
actual = 0
|
||||
# 局部阶段变量: 该刻库是否已算出**确定**结算。只服务于取消分支的兜底取值,
|
||||
# 不进任何签名/配置/遥测; 未分类异常逃逸时它无人读取, 故仍逐字走旧的全额退还。
|
||||
settlement_known = False
|
||||
# 登记在 transport 调用**之前**(1.3.5 设计 §4): 失败与取消的尝试同样
|
||||
# "真的打出去了",挪到成功之后会让诊断最需要看见的那几次从计数里消失。
|
||||
# 上下文为 None = 库内现场构造的请求,跳过而不是报错
|
||||
@@ -301,6 +304,8 @@ class RetryMW:
|
||||
actual = source.effective_est_tokens()
|
||||
else:
|
||||
actual = result.prompt_tokens + result.completion_tokens
|
||||
# 真实 usage 恰为 0 也是**已知事实**, 后续取消不得把它改写成 est
|
||||
settlement_known = True
|
||||
await self._record_quietly(self._breaker.record_success(entry))
|
||||
await self._record_quietly(self._quota.mark_progress())
|
||||
self._feed_outcome(source.name, ok=True)
|
||||
@@ -309,15 +314,20 @@ class RetryMW:
|
||||
await self._emit(request, source, call_id, started, response=response)
|
||||
return response
|
||||
except RequestRejectedError as exc:
|
||||
actual, settlement_known = 0, True # 逐字保住 1.3.5: 坏请求全额退还
|
||||
await self._on_rejected(exc, source, entry)
|
||||
await self._emit(request, source, call_id, started, error=exc)
|
||||
raise
|
||||
except ResultInvalidError as exc:
|
||||
actual, settlement_known = 0, True # 同上, 本版不改这一族记账口径
|
||||
# 坏结果 ≠ 坏服务: 熔断记成功但不计窗口样本,亦不喂健康分(M2.5 §3.1)
|
||||
await self._record_quietly(self._breaker.record_success(entry, count_attempt=False))
|
||||
await self._emit(request, source, call_id, started, error=exc)
|
||||
raise
|
||||
except asyncio.CancelledError:
|
||||
if not settlement_known:
|
||||
# 端口已开始、结算未定: 保守保留预扣(宁多扣不凭空退款, 见设计 §6.3 S3)
|
||||
actual = source.effective_est_tokens()
|
||||
if entry.is_probe:
|
||||
await self._record_quietly(self._breaker.release_probe(entry))
|
||||
await self._emit(request, source, call_id, started, error="cancelled")
|
||||
@@ -330,10 +340,12 @@ class RetryMW:
|
||||
self._feed_outcome(source.name, ok=False)
|
||||
if reason == "rate_limited":
|
||||
self._pacer.on_backpressure(source.name)
|
||||
# 结算决定必须在本分支**第一个 await 之前**定死: 同级的 except CancelledError
|
||||
# 接不住落在本块 await 上的取消,它会直穿 finally——那一刻 actual 是什么就结什么。
|
||||
# 值与 1.3.5 逐字相同(dead 全额退、瞬时保留预扣),只是算得更早。
|
||||
actual = 0 if dead else source.effective_est_tokens()
|
||||
settlement_known = True
|
||||
await self._record_quietly(self._breaker.record_failure(entry, reason, dead))
|
||||
if not dead:
|
||||
# 保守: 失败请求可能已被网关计费(CHS 同款);与入场预扣同源取值
|
||||
actual = source.effective_est_tokens()
|
||||
await self._emit(request, source, call_id, started, error=exc)
|
||||
return _Failed(exc, immediate=dead)
|
||||
finally:
|
||||
|
||||
@@ -446,6 +446,8 @@ class OcrClient:
|
||||
)
|
||||
return _FailedAttempt(exc, immediate=dead)
|
||||
finally:
|
||||
# OCR 的 0 token 是**事实**而非"未知"(设计 §6.3 S6): 故取消也恰恰结 0,
|
||||
# 不引入 chat/embedding 那套 settlement_known 兜底。
|
||||
await settle_and_release(permit, 0)
|
||||
|
||||
async def _invoke(
|
||||
|
||||
Reference in New Issue
Block a user