Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/coding/proxy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,19 @@ def status(
def usage(
days: int = typer.Option(7, "--days", "-d", help="统计天数"),
backend: Optional[str] = typer.Option(None, "--backend", "-b", help="过滤后端"),
model: Optional[str] = typer.Option(None, "--model", "-m", help="过滤请求模型"),
db_path: Optional[str] = typer.Option(None, "--db", help="数据库路径"),
) -> None:
"""查看 Token 使用统计."""
cfg = load_config(Path(db_path) if db_path else None)
logger = TokenLogger(cfg.db_path)
asyncio.run(_run_usage(logger, days, backend))
asyncio.run(_run_usage(logger, days, backend, model))


async def _run_usage(logger: TokenLogger, days: int, backend: str | None) -> None:
async def _run_usage(logger: TokenLogger, days: int, backend: str | None,
model: str | None) -> None:
await logger.init()
await show_usage(logger, days, backend)
await show_usage(logger, days, backend, model)
await logger.close()


Expand Down
11 changes: 8 additions & 3 deletions src/coding/proxy/logging/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ async def log(self, backend: str, model_requested: str, model_served: str,
duration_ms, success, failover, request_id))
await self._db.commit()

async def query_daily(self, days: int = 7, backend: str | None = None) -> list[dict]:
async def query_daily(self, days: int = 7, backend: str | None = None,
model: str | None = None) -> list[dict]:
if not self._db:
return []
sql = """SELECT date(ts) AS date, backend,
sql = """SELECT date(ts) AS date, backend, model_requested, model_served,
COUNT(*) AS total_requests,
SUM(input_tokens) AS total_input,
SUM(output_tokens) AS total_output,
Expand All @@ -73,7 +74,11 @@ async def query_daily(self, days: int = 7, backend: str | None = None) -> list[d
if backend:
sql += " AND backend = ?"
params.append(backend)
sql += " GROUP BY date(ts), backend ORDER BY date(ts) DESC, backend"
if model:
sql += " AND model_requested = ?"
params.append(model)
sql += (" GROUP BY date(ts), backend, model_requested, model_served"
" ORDER BY date(ts) DESC, backend, model_requested, model_served")
cursor = await self._db.execute(sql, params)
rows = await cursor.fetchall()
return [dict(row) for row in rows]
Expand Down
9 changes: 7 additions & 2 deletions src/coding/proxy/logging/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from .db import TokenLogger


async def show_usage(logger: TokenLogger, days: int = 7, backend: str | None = None) -> None:
async def show_usage(logger: TokenLogger, days: int = 7, backend: str | None = None,
model: str | None = None) -> None:
"""展示 Token 使用统计."""
console = Console()
rows = await logger.query_daily(days=days, backend=backend)
rows = await logger.query_daily(days=days, backend=backend, model=model)

if not rows:
console.print("[yellow]暂无使用记录[/yellow]")
Expand All @@ -20,6 +21,8 @@ async def show_usage(logger: TokenLogger, days: int = 7, backend: str | None = N
table = Table(title=f"Token 使用统计(最近 {days} 天)")
table.add_column("日期", style="cyan")
table.add_column("后端", style="green")
table.add_column("请求模型", style="magenta")
table.add_column("实际模型", style="yellow")
table.add_column("请求数", justify="right")
table.add_column("输入 Token", justify="right", style="blue")
table.add_column("输出 Token", justify="right", style="blue")
Expand All @@ -30,6 +33,8 @@ async def show_usage(logger: TokenLogger, days: int = 7, backend: str | None = N
table.add_row(
str(row.get("date", "")),
str(row.get("backend", "")),
str(row.get("model_requested", "")),
str(row.get("model_served", "")),
str(row.get("total_requests", 0)),
str(row.get("total_input", 0)),
str(row.get("total_output", 0)),
Expand Down
69 changes: 69 additions & 0 deletions tests/test_token_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,72 @@ async def test_query_window_total_excludes_failures(logger):
)
total = await logger.query_window_total(5.0)
assert total == 150 # 失败请求不计入


@pytest.mark.asyncio
async def test_query_daily_groups_by_model(logger):
"""query_daily 应按 model_requested 和 model_served 分组."""
await logger.log(
backend="anthropic", model_requested="claude-sonnet-4",
model_served="claude-sonnet-4",
input_tokens=100, output_tokens=50,
)
await logger.log(
backend="anthropic", model_requested="claude-opus-4",
model_served="claude-opus-4",
input_tokens=200, output_tokens=80,
)
await logger.log(
backend="anthropic", model_requested="claude-sonnet-4",
model_served="claude-sonnet-4",
input_tokens=150, output_tokens=60,
)
rows = await logger.query_daily(days=7)
assert len(rows) == 2
models = {r["model_requested"] for r in rows}
assert models == {"claude-sonnet-4", "claude-opus-4"}
# 验证 sonnet 聚合正确
sonnet = next(r for r in rows if r["model_requested"] == "claude-sonnet-4")
assert sonnet["total_requests"] == 2
assert sonnet["total_input"] == 250
assert sonnet["total_output"] == 110


@pytest.mark.asyncio
async def test_query_daily_model_filter(logger):
"""query_daily 的 model 参数应正确过滤."""
await logger.log(
backend="anthropic", model_requested="claude-sonnet-4",
model_served="claude-sonnet-4",
input_tokens=100, output_tokens=50,
)
await logger.log(
backend="anthropic", model_requested="claude-opus-4",
model_served="claude-opus-4",
input_tokens=200, output_tokens=80,
)
rows = await logger.query_daily(days=7, model="claude-opus-4")
assert len(rows) == 1
assert rows[0]["model_requested"] == "claude-opus-4"
assert rows[0]["total_requests"] == 1


@pytest.mark.asyncio
async def test_query_daily_shows_model_mapping(logger):
"""故障转移场景:model_requested 与 model_served 不同时应分别展示."""
await logger.log(
backend="zhipu", model_requested="claude-sonnet-4",
model_served="glm-5.1",
input_tokens=300, output_tokens=100, failover=True,
)
await logger.log(
backend="anthropic", model_requested="claude-sonnet-4",
model_served="claude-sonnet-4",
input_tokens=100, output_tokens=50,
)
rows = await logger.query_daily(days=7)
assert len(rows) == 2
zhipu_row = next(r for r in rows if r["backend"] == "zhipu")
assert zhipu_row["model_requested"] == "claude-sonnet-4"
assert zhipu_row["model_served"] == "glm-5.1"
assert zhipu_row["total_failovers"] == 1