mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 02:05:57 +08:00
fix(proxy): correct usage accounting on format-conversion paths
Audited all proxy format-conversion paths (Chat<->Message, Chat<->Response, Gemini<->Message) for usage/cache metering. Five issues found and fixed. The dedup mechanism (request_id PK, proxy/session source isolation) is untouched, so no double-counting is introduced. - A (Claude + openai_chat, streaming): inject stream_options.include_usage so OpenAI-compatible upstreams emit usage in the SSE tail. Without it the converted Anthropic message_delta was all-zero and the whole request's input/output/cache was dropped. Same root cause as the already-fixed Codex Chat path; the injection is extracted into a shared helper (transform::inject_openai_stream_include_usage) reused by both paths. - C (Claude + gemini_native): subtract cachedContentTokenCount from input_tokens in build_anthropic_usage so input becomes fresh input (Anthropic semantics). Previously the cache-hit tokens were billed twice because this path meters as app_type="claude" (input_includes_cache_read = false) while Gemini's promptTokenCount includes the cache. - D (Codex + openai_chat, streaming): gate log_usage on has_billable_tokens() to skip the synthetic all-zero usage the converter emits when a non-compliant upstream omits usage, preventing empty-row request-count inflation. - P2 (from_claude_stream_events): use has_billable_tokens() for the return gate instead of input>0||output>0, so a fully-cached streamed request (cache_read>0, input==output==0) is still recorded. Affects all Claude-streaming paths, not just Gemini. - P3 (Codex Chat->Responses, non-streaming): apply the same has_billable_tokens() filter the streaming branch got, since the synthesized all-zero usage makes from_codex_response return Some and bypass the `if let Some` guard. Add TokenUsage::has_billable_tokens() as the unified predicate. New tests cover include_usage injection, gemini input subtraction, the gate itself, cache-only stream recording, and synthetic all-zero codex usage. Full lib suite: 1569 passed.
This commit is contained in:
@@ -765,6 +765,15 @@ async fn handle_codex_chat_to_responses_transform(
|
||||
move |events, first_token_ms| {
|
||||
let usage =
|
||||
TokenUsage::from_codex_stream_events_auto(&events).unwrap_or_default();
|
||||
// 上游遵守 OpenAI 语义省略 usage 时,Chat→Responses 转换器会合成一个
|
||||
// 全 0 的 response.completed,from_codex_response 对 input/output 字段
|
||||
// 存在(哪怕=0)即返回 Some。缺 nonzero 闸门会让全 0 usage 也被写入:
|
||||
// message_id=None → dedup_request_id 退化为随机 UUID,无法去重,每笔
|
||||
// 请求插入一条无意义空行、虚增请求数。对齐 Claude transform handler 的 skip。
|
||||
if !usage.has_billable_tokens() {
|
||||
log::debug!("[Codex] 流式响应 usage 全 0 或缺失,跳过消费记录");
|
||||
return;
|
||||
}
|
||||
let model = usage.model.clone().unwrap_or_else(|| request_model.clone());
|
||||
let latency_ms = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
@@ -844,7 +853,13 @@ async fn handle_codex_chat_to_responses_transform(
|
||||
.record_response(&responses_response)
|
||||
.await;
|
||||
|
||||
if let Some(usage) = TokenUsage::from_codex_response_auto(&responses_response) {
|
||||
// 上游非流式 Chat 省略 usage 时,chat_usage_to_responses_usage 会合成全 0 usage
|
||||
// (transform_codex_chat.rs:1581),from_codex_response 对 input/output 字段存在(哪怕=0)
|
||||
// 即返回 Some。用 has_billable_tokens 闸门跳过全 0,避免空行虚增请求数——与流式分支
|
||||
// 及 Claude transform handler 的 skip 行为对齐。
|
||||
if let Some(usage) = TokenUsage::from_codex_response_auto(&responses_response)
|
||||
.filter(TokenUsage::has_billable_tokens)
|
||||
{
|
||||
let model = responses_response
|
||||
.get("model")
|
||||
.and_then(|m| m.as_str())
|
||||
|
||||
Reference in New Issue
Block a user