Skip to content

Commit ceca323

Browse files
committed
refactor(usage): 按 model_served 聚合同日同供应商的 usage 统计记录;
将 query_daily 的 GROUP BY 维度从 (date, vendor, model_requested, model_served) 降为三维 (date, vendor, model_served),使用 GROUP_CONCAT(DISTINCT) 将多个 请求模型合并为逗号分隔列表,消除同日同供应商同实际模型的冗余行。 - db.py: SQL 层改用 GROUP_CONCAT(DISTINCT model_requested) 聚合 - stats.py: 展示层复用 _format_model_display 适配多值场景 - test_token_logger.py: 新增 5 个聚合场景测试(核心聚合/单值边界/ DISTINCT 去重/过滤兼容/供应商隔离),971 测试全量通过零回归 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
1 parent ca63bc7 commit ceca323

3 files changed

Lines changed: 130 additions & 4 deletions

File tree

‎src/coding/proxy/logging/db.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ async def query_daily(
255255
return []
256256
days = max(1, days)
257257
start_iso = _days_start_utc_iso(days)
258-
sql = """SELECT local_date(ts) AS date, vendor, model_requested, model_served,
258+
sql = """SELECT local_date(ts) AS date, vendor,
259+
GROUP_CONCAT(DISTINCT model_requested) AS model_requested,
260+
model_served,
259261
COUNT(*) AS total_requests,
260262
SUM(input_tokens) AS total_input,
261263
SUM(output_tokens) AS total_output,
@@ -272,8 +274,8 @@ async def query_daily(
272274
sql += " AND model_requested = ?"
273275
params.append(model)
274276
sql += (
275-
" GROUP BY local_date(ts), vendor, model_requested, model_served"
276-
" ORDER BY local_date(ts) DESC, vendor, model_requested, model_served"
277+
" GROUP BY local_date(ts), vendor, model_served"
278+
" ORDER BY local_date(ts) DESC, vendor, model_served"
277279
)
278280
cursor = await self._db.execute(sql, params)
279281
rows = await cursor.fetchall()

‎src/coding/proxy/logging/stats.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def show_usage(
101101
table.add_row(
102102
str(row.get("date", "")),
103103
vendor_name,
104-
str(row.get("model_requested", "")),
104+
_format_model_display(row.get("model_requested")),
105105
model_served,
106106
str(row.get("total_requests", 0)),
107107
_format_tokens(total_input),

‎tests/test_token_logger.py‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,127 @@ async def test_query_daily_clamps_zero_days(logger):
491491
# days=0 不应报错,行为等同 days=1
492492
rows = await logger.query_daily(days=0)
493493
assert len(rows) >= 1
494+
495+
496+
# ---------------------------------------------------------------------------
497+
# 按 model_served 聚合测试(同日/同供应商/同实际模型 → 合并为一行)
498+
# ---------------------------------------------------------------------------
499+
500+
501+
@pytest.mark.asyncio
502+
async def test_query_daily_aggregates_by_model_served(logger):
503+
"""核心场景:同日/同供应商/同 model_served 但不同 model_requested 应聚为一行."""
504+
await logger.log(
505+
vendor="anthropic",
506+
model_requested="claude-opus-4-6",
507+
model_served="claude-sonnet-4-6",
508+
input_tokens=100,
509+
output_tokens=50,
510+
)
511+
await logger.log(
512+
vendor="anthropic",
513+
model_requested="claude-sonnet-4-6",
514+
model_served="claude-sonnet-4-6",
515+
input_tokens=200,
516+
output_tokens=80,
517+
)
518+
rows = await logger.query_daily(days=7)
519+
# 同一 model_served → 聚为一行
520+
assert len(rows) == 1
521+
row = rows[0]
522+
# model_requested 应包含两个值,逗号分隔
523+
requested_models = set(row["model_requested"].split(","))
524+
assert requested_models == {"claude-opus-4-6", "claude-sonnet-4-6"}
525+
# Token 统计应正确求和
526+
assert row["total_requests"] == 2
527+
assert row["total_input"] == 300
528+
assert row["total_output"] == 130
529+
530+
531+
@pytest.mark.asyncio
532+
async def test_query_daily_single_model_no_comma(logger):
533+
"""边界:仅一个 model_requested 时不应出现多余逗号."""
534+
await logger.log(
535+
vendor="anthropic",
536+
model_requested="claude-sonnet-4-6",
537+
model_served="claude-sonnet-4-6",
538+
input_tokens=100,
539+
output_tokens=50,
540+
)
541+
rows = await logger.query_daily(days=7)
542+
assert len(rows) == 1
543+
# 单值时应为纯字符串,不含逗号
544+
assert rows[0]["model_requested"] == "claude-sonnet-4-6"
545+
assert "," not in rows[0]["model_requested"]
546+
547+
548+
@pytest.mark.asyncio
549+
async def test_query_daily_distinct_deduplication(logger):
550+
"""DISTINCT 应去重相同的 model_requested."""
551+
await logger.log(
552+
vendor="anthropic",
553+
model_requested="claude-sonnet-4-6",
554+
model_served="claude-sonnet-4-6",
555+
input_tokens=100,
556+
output_tokens=50,
557+
)
558+
await logger.log(
559+
vendor="anthropic",
560+
model_requested="claude-sonnet-4-6", # 相同的 model_requested
561+
model_served="claude-sonnet-4-6",
562+
input_tokens=200,
563+
output_tokens=80,
564+
)
565+
rows = await logger.query_daily(days=7)
566+
assert len(rows) == 1
567+
# 去重后不应重复出现
568+
assert rows[0]["model_requested"] == "claude-sonnet-4-6"
569+
assert rows[0]["model_requested"].count(",") == 0
570+
571+
572+
@pytest.mark.asyncio
573+
async def test_query_daily_model_filter_still_works(logger):
574+
"""--model/-m 过滤在聚合前执行,行为不变."""
575+
await logger.log(
576+
vendor="anthropic",
577+
model_requested="claude-opus-4-6",
578+
model_served="claude-sonnet-4-6",
579+
input_tokens=100,
580+
output_tokens=50,
581+
)
582+
await logger.log(
583+
vendor="anthropic",
584+
model_requested="claude-sonnet-4-6",
585+
model_served="claude-sonnet-4-6",
586+
input_tokens=200,
587+
output_tokens=80,
588+
)
589+
# 按 model_requested 过滤,仅保留 opus 的记录
590+
rows = await logger.query_daily(days=7, model="claude-opus-4-6")
591+
assert len(rows) == 1
592+
assert rows[0]["total_requests"] == 1
593+
assert rows[0]["total_input"] == 100
594+
595+
596+
@pytest.mark.asyncio
597+
async def test_query_daily_different_vendors_not_merged(logger):
598+
"""不同供应商即使 model_served 相同也不应合并."""
599+
await logger.log(
600+
vendor="anthropic",
601+
model_requested="claude-sonnet-4-6",
602+
model_served="claude-sonnet-4-6",
603+
input_tokens=100,
604+
output_tokens=50,
605+
)
606+
await logger.log(
607+
vendor="zhipu",
608+
model_requested="claude-sonnet-4-6",
609+
model_served="claude-sonnet-4-6", # 同一 model_served
610+
input_tokens=200,
611+
output_tokens=80,
612+
)
613+
rows = await logger.query_daily(days=7)
614+
# 不同供应商,应为两行
615+
assert len(rows) == 2
616+
vendors = {r["vendor"] for r in rows}
617+
assert vendors == {"anthropic", "zhipu"}

0 commit comments

Comments
 (0)