Skip to content

Commit 52a9b84

Browse files
authored
Merge pull request #140 from ThreeFish-AI/feature/1.x.x
v0.2.0
2 parents cbe95fc + 4ed4be0 commit 52a9b84

39 files changed

Lines changed: 3893 additions & 391 deletions

‎CHANGELOG.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44

55
## [Unreleased]
66

7+
## [v0.2.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.0) — 2026-04-09
8+
9+
> [!IMPORTANT]
10+
>
11+
> **🚀 供应商大扩军 × 用量仪表盘全面进化,双线暴击!**
12+
>
13+
> 卡在一家供应商的限额天花板下抬不起头?现在你手握 **九条命**——新增 MiniMax、小米 MiMo、阿里千问、Kimi、豆包五路援军,全部原生讲 Anthropic 话,无缝接入 N-tier。 Token 烧到哪儿心里没数?新版 `usage` 命令解锁日/周/月/全量四档视角,多供应商并排比,汇总行一行看全局。**备用仓更满,账单更透,从此宕机只是别人家的故事。**
14+
15+
### ✨ 核心亮点
16+
17+
- **5 家供应商集体入场**:MiniMax、小米 MiMo、阿里千问、Kimi、豆包(火山引擎)正式入编 N-tier。备用通道数量直接翻倍,不怕堵;
18+
- **`usage` 命令全面升级**:从"只有天数"进化为**日 / 周 / 月 / 全量**四档时间维度(`-d 7` / `-w` / `-m` / `-t`)。支持多值过滤——`-v anthropic,kimi` 或 `--model claude-opus-4-6,glm-5.1` 用逗号隔开随便选。表格末行自动追加**汇总行**,请求总量、Token 总计、总成本、加权平均延迟四项一览无余。Token 花在哪家、烧了多少、谁最能扛——这张表给你答案;
19+
20+
### 🔧 更多特性
21+
22+
- **品牌横幅正式上线**:`proxy start` 启动时打印 Coding Proxy 专属 ASCII Banner 与版本号,告别冷冰冰的裸日志起手式;
23+
- **529 过载纳入降级触发**:HTTP `529 overloaded_error` 正式加入故障转移白名单,Anthropic 喊"我堵了"时 Proxy 不再干等;
24+
- **Zhipu 跨供应商级联故障根治**:`Internal Network Failure` 纳入 500 降级条件;`tool_result` 角色错位导致的下游级联崩溃彻底斩断,再也不因历史 message 的"历史遗留问题"把整条链拖下水;
25+
726
## [v0.1.3](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.1.3) — 2026-04-07
827

928
> [!IMPORTANT]

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "coding-proxy"
3-
version = "0.1.3"
3+
version = "0.2.0"
44
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM."
55
readme = "README.md"
66
requires-python = ">=3.12"

‎src/coding/proxy/cli/__init__.py‎

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import asyncio
99
import logging
1010
from pathlib import Path
11-
from typing import TYPE_CHECKING, Optional
11+
from typing import TYPE_CHECKING
1212

1313
import typer
1414

@@ -18,7 +18,7 @@
1818
from rich.console import Console
1919

2020
from ..config.loader import load_config
21-
from ..logging.db import TokenLogger
21+
from ..logging.db import TimePeriod, TokenLogger
2222
from ..logging.stats import show_usage
2323
from .auth_commands import app as auth_app
2424
from .auth_commands import auto_login_if_needed as _auto_login_if_needed
@@ -48,6 +48,29 @@ def _build_token_store(cfg_path: Path | None = None):
4848
return cfg, store
4949

5050

51+
def _resolve_period(
52+
*,
53+
days: int = 7,
54+
week: int | None = None,
55+
month: int | None = None,
56+
total: bool = False,
57+
) -> tuple[TimePeriod, int]:
58+
"""将互斥的时间维度标志解析为 (TimePeriod, count) 元组.
59+
60+
优先级: ``-t`` > ``-m`` > ``-w`` > ``-d``。
61+
62+
Returns:
63+
``(period, count)`` 元组。``count`` 表示查询最近第 N 个周期的数据。
64+
"""
65+
if total:
66+
return TimePeriod.TOTAL, 1
67+
if month is not None:
68+
return TimePeriod.MONTH, max(1, month)
69+
if week is not None:
70+
return TimePeriod.WEEK, max(1, week)
71+
return TimePeriod.DAY, max(1, days)
72+
73+
5174
# ── 主命令 ─────────────────────────────────────────────────────
5275

5376

@@ -61,6 +84,7 @@ def start(
6184
import uvicorn
6285

6386
from ..server.app import create_app
87+
from .banner import print_banner
6488

6589
cfg_path = _resolve_config_path(config)
6690
cfg = load_config(cfg_path)
@@ -76,6 +100,10 @@ def start(
76100
from ..logging import build_log_config
77101

78102
fastapi_app = create_app(cfg)
103+
104+
# 打印启动品牌横幅
105+
print_banner(console, host=cfg.server.host, port=cfg.server.port)
106+
79107
uvicorn.run(
80108
fastapi_app,
81109
host=cfg.server.host,
@@ -113,29 +141,68 @@ def status(
113141

114142
@app.command()
115143
def usage(
116-
days: int = typer.Option(7, "--days", "-d", help="统计天数"),
144+
days: int = typer.Option(7, "--days", "-d", help="统计天数(与 -w/-m/-t 互斥)"),
145+
week: int | None = typer.Option(
146+
None, "--week", "-w", help="最近第 N 周统计(按周聚合,默认 1)"
147+
),
148+
month: int | None = typer.Option(
149+
None, "--month", "-m", help="最近第 N 月统计(按月聚合,默认 1)"
150+
),
151+
total: bool = typer.Option(False, "--total", "-t", help="统计全部历史记录"),
117152
vendor: str | None = typer.Option(None, "--vendor", "-v", help="过滤供应商"),
118-
model: str | None = typer.Option(None, "--model", "-m", help="过滤请求模型"),
153+
model: str | None = typer.Option(
154+
None, "--model", help="过滤实际服务模型(model_served),逗号分隔可指定多个"
155+
),
119156
db_path: str | None = typer.Option(None, "--db", help="数据库路径"),
120157
) -> None:
121-
"""查看 Token 使用统计."""
158+
"""查看 Token 使用统计.
159+
160+
时间维度(互斥,优先级 -t > -m > -w > -d):
161+
162+
\b
163+
-d 7 最近 7 天(默认,按日聚合)
164+
-w [N] 最近第 N 周(按周聚合,默认 1=本周)
165+
-m [N] 最近第 N 月(按月聚合,默认 1=本月)
166+
-t 全部历史(按供应商+模型聚合)
167+
"""
168+
period, count = _resolve_period(days=days, week=week, month=month, total=total)
122169
cfg = load_config(Path(db_path) if db_path else None)
123170
token_logger = TokenLogger(cfg.db_path)
124-
asyncio.run(_run_usage(token_logger, days, vendor, model, cfg))
171+
# 解析逗号分隔的多 vendor(如 "anthropic,zhipu" → ["anthropic", "zhipu"])
172+
vendor_filter: str | list[str] | None = None
173+
if vendor:
174+
parts = [v.strip() for v in vendor.split(",") if v.strip()]
175+
vendor_filter = parts[0] if len(parts) == 1 else parts
176+
# 解析逗号分隔的多 model(如 "glm-5,glm-5.1" → ["glm-5", "glm-5.1"])
177+
model_filter: str | list[str] | None = None
178+
if model:
179+
parts = [m.strip() for m in model.split(",") if m.strip()]
180+
model_filter = parts[0] if len(parts) == 1 else parts
181+
asyncio.run(
182+
_run_usage(token_logger, period, count, vendor_filter, model_filter, cfg)
183+
)
125184

126185

127186
async def _run_usage(
128187
token_logger: TokenLogger,
129-
days: int,
130-
vendor: str | None,
131-
model: str | None,
188+
period: TimePeriod,
189+
count: int,
190+
vendor: str | list[str] | None,
191+
model: str | list[str] | None,
132192
cfg: ProxyConfig,
133193
) -> None:
134194
from ..pricing import PricingTable
135195

136196
await token_logger.init()
137197
pricing_table = PricingTable(cfg.pricing)
138-
await show_usage(token_logger, days, vendor, model, pricing_table)
198+
await show_usage(
199+
token_logger,
200+
vendor=vendor,
201+
model=model,
202+
pricing_table=pricing_table,
203+
period=period,
204+
count=count,
205+
)
139206
await token_logger.close()
140207

141208

‎src/coding/proxy/cli/banner.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""CLI 启动品牌横幅.
2+
3+
使用 Rich Panel 构建紧凑的品牌展示面板,
4+
在服务启动前向终端输出版本与监听地址信息。
5+
6+
设计原则:
7+
- 正交分解: 展示逻辑独立于业务逻辑
8+
- 复用驱动: 使用项目已有的 Rich Console 模式
9+
- 最小干预: 无新依赖,纯组合现有组件
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from typing import TYPE_CHECKING
15+
16+
from rich.panel import Panel
17+
from rich.text import Text
18+
19+
if TYPE_CHECKING:
20+
from rich.console import Console
21+
22+
from .. import __version__
23+
24+
25+
def build_banner(host: str = "127.0.0.1", port: int = 8046) -> Panel:
26+
"""构建品牌横幅 Panel.
27+
28+
Args:
29+
host: 监听地址.
30+
port: 监听端口.
31+
32+
Returns:
33+
配置完成的 Rich Panel 实例.
34+
"""
35+
brand_text = Text()
36+
brand_text.append("⚡ ", style="bold")
37+
brand_text.append("Coding Proxy", style="bold cyan")
38+
brand_text.append("\n\n")
39+
40+
info_text = Text()
41+
info_text.append("Version: ", style="dim")
42+
info_text.append(__version__, style="green")
43+
info_text.append(" │ ", style="dim")
44+
info_text.append("Listening: ", style="dim")
45+
info_text.append(f"http://{host}:{port}", style="blue")
46+
brand_text.append(info_text)
47+
48+
return Panel(brand_text, border_style="cyan", padding=(1, 2), expand=False)
49+
50+
51+
def print_banner(console: Console, host: str = "127.0.0.1", port: int = 8046) -> None:
52+
"""通过指定 Console 打印品牌横幅.
53+
54+
Args:
55+
console: Rich Console 实例(复用 CLI 层已有实例).
56+
host: 监听地址.
57+
port: 监听端口.
58+
"""
59+
console.print()
60+
console.print(build_banner(host=host, port=port))
61+
console.print()

0 commit comments

Comments
 (0)