coding-proxy 0.2.3a5__tar.gz → 0.2.4a2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/CHANGELOG.md +1 -1
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/PKG-INFO +5 -1
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/README.md +4 -0
- coding_proxy-0.2.4a2/assets/dashboard-v0.2.3.png +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/docs/zh-CN/README.md +4 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/pyproject.toml +1 -1
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/config.default.yaml +6 -6
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/logging/stats.py +1 -1
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/pricing.py +2 -2
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/executor.py +96 -2
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/router.py +8 -2
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/server/dashboard.py +392 -128
- coding_proxy-0.2.4a2/src/coding/proxy/server/request_normalizer.py +541 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/server/routes.py +17 -4
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/anthropic.py +2 -53
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_app_routes.py +10 -10
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_currency.py +3 -3
- coding_proxy-0.2.4a2/tests/test_request_normalizer.py +983 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_router_executor.py +191 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_vendors.py +37 -76
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/uv.lock +1 -1
- coding_proxy-0.2.3a5/src/coding/proxy/server/request_normalizer.py +0 -288
- coding_proxy-0.2.3a5/tests/test_request_normalizer.py +0 -368
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/.gitignore +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/AGENTS.md +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/CLAUDE.md +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/LICENSE +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/docs/ci-cd.md +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/docs/framework.md +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/docs/user-guide.md +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/__init__.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_banner.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_compat.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_config_init.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_copilot.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_mixins.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_token.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_pricing.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_schema.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_tier.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_time_range.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_types.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.2.3a5 → coding_proxy-0.2.4a2}/tests/test_zhipu.py +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
## [v0.2.3](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.
|
|
7
|
+
## [v0.2.3](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.3) — 2026-04-16
|
|
8
8
|
|
|
9
9
|
- feat(dashboard): 新增实时 Web Dashboard 页面,聚合展示流量与用量统计;
|
|
10
10
|
- docs(user-guide): 补充 POST /v1/messages 完整 API 参考文档;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4a2
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
6
|
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
@@ -56,6 +56,10 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
|
|
|
56
56
|
|
|
57
57
|
## 🌟 Core Features
|
|
58
58
|
|
|
59
|
+
<div align="center">
|
|
60
|
+
<img src="assets/dashboard-v0.2.3.png">
|
|
61
|
+
</div>
|
|
62
|
+
|
|
59
63
|
- **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
|
|
60
64
|
- **🛡️ Smart Resilience & Quota Guardians**: Every single vendor node comes fully armed with an independent **Circuit Breaker** and **Quota Guard** to proactively dodge avalanches without breaking a sweat.
|
|
61
65
|
- **👻 Phantom-like Transparency**: **100% transparent** to the client! No code tweaks required. Overwrite `ANTHROPIC_BASE_URL` with a single line, and you're good to go.
|
|
@@ -29,6 +29,10 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
|
|
|
29
29
|
|
|
30
30
|
## 🌟 Core Features
|
|
31
31
|
|
|
32
|
+
<div align="center">
|
|
33
|
+
<img src="assets/dashboard-v0.2.3.png">
|
|
34
|
+
</div>
|
|
35
|
+
|
|
32
36
|
- **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
|
|
33
37
|
- **🛡️ Smart Resilience & Quota Guardians**: Every single vendor node comes fully armed with an independent **Circuit Breaker** and **Quota Guard** to proactively dodge avalanches without breaking a sweat.
|
|
34
38
|
- **👻 Phantom-like Transparency**: **100% transparent** to the client! No code tweaks required. Overwrite `ANTHROPIC_BASE_URL` with a single line, and you're good to go.
|
|
Binary file
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
|
|
30
30
|
## 🌟 核心特性 (Core Features)
|
|
31
31
|
|
|
32
|
+
<div align="center">
|
|
33
|
+
<img src="../../assets/dashboard-v0.2.3.png">
|
|
34
|
+
</div>
|
|
35
|
+
|
|
32
36
|
- **⛓️ N-tier 链式故障转移 (Failover)**:自主降序序列,支持 Claude 官方 Plans,以及 GitHub Copilot、智谱、MiniMax、阿里千问、小米、Kimi、豆包等的 Coding Plan。
|
|
33
37
|
- **🛡️ 智能弹性与容灾守卫**:每个供应商节点独立配备 **熔断器 (Circuit Breaker)** 与 **配额守卫 (Quota Guard)**,防雪崩、主动避险。
|
|
34
38
|
- **👻 透明无感代理机制**:对客户端 **100% 透明**!无需修改任何代码,仅需一行配置覆盖 `ANTHROPIC_BASE_URL` 即可接入。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4a2"
|
|
4
4
|
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao..."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -51,14 +51,14 @@ vendors:
|
|
|
51
51
|
recovery_timeout_seconds: 300
|
|
52
52
|
success_threshold: 2
|
|
53
53
|
quota_guard:
|
|
54
|
-
enabled:
|
|
55
|
-
token_budget:
|
|
54
|
+
enabled: false
|
|
55
|
+
token_budget: 60000000 # 5 小时 token 预算(根据订阅计划调整)
|
|
56
56
|
window_hours: 5.0
|
|
57
57
|
threshold_percent: 99.0
|
|
58
58
|
probe_interval_seconds: 300
|
|
59
59
|
weekly_quota_guard:
|
|
60
|
-
enabled:
|
|
61
|
-
token_budget:
|
|
60
|
+
enabled: false
|
|
61
|
+
token_budget: 800000000 # 一周 token 预算(根据订阅计划调整)
|
|
62
62
|
window_hours: 168.0 # 7 天滑动窗口
|
|
63
63
|
threshold_percent: 99.0
|
|
64
64
|
probe_interval_seconds: 1800 # 每 30 分钟探测一次
|
|
@@ -84,7 +84,7 @@ vendors:
|
|
|
84
84
|
|
|
85
85
|
# Vendor 2: Google Antigravity Plans(中间层,默认禁用)
|
|
86
86
|
- vendor: antigravity
|
|
87
|
-
enabled:
|
|
87
|
+
enabled: false # 启用需配置 OAuth 凭据
|
|
88
88
|
client_id: "${GOOG_CLIENT_ID}" # Google OAuth2 Client ID
|
|
89
89
|
client_secret: "${GOOG_CLIENT_SECRET}" # Google OAuth2 Client Secret
|
|
90
90
|
refresh_token: "${GOOG_REFRESH_TOKEN}" # Google OAuth2 Refresh Token
|
|
@@ -96,7 +96,7 @@ vendors:
|
|
|
96
96
|
recovery_timeout_seconds: 300
|
|
97
97
|
success_threshold: 2
|
|
98
98
|
quota_guard:
|
|
99
|
-
enabled:
|
|
99
|
+
enabled: false # 启用后按 Premium Requests 配额管理
|
|
100
100
|
token_budget: 0
|
|
101
101
|
window_hours: 24.0
|
|
102
102
|
threshold_percent: 95.0
|
|
@@ -50,8 +50,8 @@ class CostValue:
|
|
|
50
50
|
amount: float
|
|
51
51
|
currency: Currency = Currency.default()
|
|
52
52
|
|
|
53
|
-
def format(self, precision: int =
|
|
54
|
-
"""格式化为 ``$0.
|
|
53
|
+
def format(self, precision: int = 2) -> str:
|
|
54
|
+
"""格式化为 ``$0.12`` 或 ``¥0.12``."""
|
|
55
55
|
return f"{self.currency.symbol}{self.amount:.{precision}f}"
|
|
56
56
|
|
|
57
57
|
@property
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import copy
|
|
9
10
|
import logging
|
|
10
11
|
import time
|
|
11
12
|
from collections.abc import AsyncIterator
|
|
@@ -222,10 +223,94 @@ class _RouteExecutor:
|
|
|
222
223
|
|
|
223
224
|
# ── 公开执行入口 ──────────────────────────────────────
|
|
224
225
|
|
|
226
|
+
def _prepare_body_for_tier(
|
|
227
|
+
self,
|
|
228
|
+
body: dict[str, Any],
|
|
229
|
+
tier: VendorTier,
|
|
230
|
+
normalization: Any = None,
|
|
231
|
+
session_record: Any = None,
|
|
232
|
+
) -> dict[str, Any]:
|
|
233
|
+
"""为指定 tier 准备请求体,必要时应用 Anthropic 专属修复(Phase 2).
|
|
234
|
+
|
|
235
|
+
仅当 tier 为 Anthropic 时才执行以下处理:
|
|
236
|
+
1. tool_result 重定位 + 孤儿修复(需 normalization.has_anthropic_fixes)
|
|
237
|
+
2. 条件化 thinking block 剥离(仅跨供应商场景)
|
|
238
|
+
|
|
239
|
+
确保 Zhipu 等其他 vendor 不受影响。
|
|
240
|
+
"""
|
|
241
|
+
if tier.name != "anthropic":
|
|
242
|
+
return body
|
|
243
|
+
|
|
244
|
+
needs_tool_fixes = (
|
|
245
|
+
normalization is not None and normalization.has_anthropic_fixes
|
|
246
|
+
)
|
|
247
|
+
needs_thinking_strip = self._needs_thinking_strip(normalization, session_record)
|
|
248
|
+
|
|
249
|
+
if not needs_tool_fixes and not needs_thinking_strip:
|
|
250
|
+
return body
|
|
251
|
+
|
|
252
|
+
from ..server.request_normalizer import (
|
|
253
|
+
apply_anthropic_specific_fixes,
|
|
254
|
+
strip_thinking_blocks,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
body_for_vendor = copy.deepcopy(body)
|
|
258
|
+
|
|
259
|
+
if needs_tool_fixes:
|
|
260
|
+
fixes = apply_anthropic_specific_fixes(
|
|
261
|
+
body_for_vendor.get("messages", []),
|
|
262
|
+
normalization.misplaced_tool_results,
|
|
263
|
+
normalization.misplaced_log_info,
|
|
264
|
+
)
|
|
265
|
+
if fixes:
|
|
266
|
+
logger.debug(
|
|
267
|
+
"Applied Anthropic-specific fixes for tier %s: %s",
|
|
268
|
+
tier.name,
|
|
269
|
+
", ".join(fixes),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if needs_thinking_strip:
|
|
273
|
+
stripped = strip_thinking_blocks(body_for_vendor)
|
|
274
|
+
if stripped:
|
|
275
|
+
logger.debug(
|
|
276
|
+
"Stripped %d thinking block(s) for cross-vendor compatibility",
|
|
277
|
+
stripped,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return body_for_vendor
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _needs_thinking_strip(normalization: Any, session_record: Any) -> bool:
|
|
284
|
+
"""判断是否需要剥离 thinking blocks(仅跨供应商场景).
|
|
285
|
+
|
|
286
|
+
信号优先级:
|
|
287
|
+
1. 请求规范化信号 — 当前请求体中检测到跨供应商产物
|
|
288
|
+
2. 会话历史信号 — provider_state 中存在非 Anthropic 供应商记录
|
|
289
|
+
|
|
290
|
+
安全默认:当无法确定会话来源时(session_record 为 None),
|
|
291
|
+
回退到始终剥离,确保与 compat_session_store 未配置时的向后兼容。
|
|
292
|
+
"""
|
|
293
|
+
# Signal 1: normalization 检测到跨供应商产物
|
|
294
|
+
if normalization is not None and normalization.has_cross_vendor_signals:
|
|
295
|
+
return True
|
|
296
|
+
# Signal 2: 无会话追踪能力 → 无法判断是否跨供应商 → 安全回退到剥离
|
|
297
|
+
if session_record is None:
|
|
298
|
+
return True
|
|
299
|
+
# Signal 3: 会话历史中有非 Anthropic 供应商
|
|
300
|
+
if session_record.provider_state:
|
|
301
|
+
non_anthropic = {
|
|
302
|
+
v for v in session_record.provider_state if v != "anthropic"
|
|
303
|
+
}
|
|
304
|
+
if non_anthropic:
|
|
305
|
+
return True
|
|
306
|
+
# 纯 Anthropic 会话,无跨供应商信号 → 保留 thinking blocks
|
|
307
|
+
return False
|
|
308
|
+
|
|
225
309
|
async def execute_stream(
|
|
226
310
|
self,
|
|
227
311
|
body: dict[str, Any],
|
|
228
312
|
headers: dict[str, str],
|
|
313
|
+
normalization: Any = None,
|
|
229
314
|
) -> AsyncIterator[tuple[bytes, str]]:
|
|
230
315
|
"""路由流式请求,按优先级尝试各层级."""
|
|
231
316
|
last_idx = len(self._tiers) - 1
|
|
@@ -257,7 +342,12 @@ class _RouteExecutor:
|
|
|
257
342
|
usage: dict[str, Any] = {}
|
|
258
343
|
|
|
259
344
|
try:
|
|
260
|
-
|
|
345
|
+
body_for_tier = self._prepare_body_for_tier(
|
|
346
|
+
body, tier, normalization, session_record=session_record
|
|
347
|
+
)
|
|
348
|
+
async for chunk in tier.vendor.send_message_stream(
|
|
349
|
+
body_for_tier, headers
|
|
350
|
+
):
|
|
261
351
|
parse_usage_from_chunk(
|
|
262
352
|
chunk,
|
|
263
353
|
usage,
|
|
@@ -389,6 +479,7 @@ class _RouteExecutor:
|
|
|
389
479
|
self,
|
|
390
480
|
body: dict[str, Any],
|
|
391
481
|
headers: dict[str, str],
|
|
482
|
+
normalization: Any = None,
|
|
392
483
|
) -> VendorResponse:
|
|
393
484
|
"""路由非流式请求,按优先级尝试各层级."""
|
|
394
485
|
last_idx = len(self._tiers) - 1
|
|
@@ -417,7 +508,10 @@ class _RouteExecutor:
|
|
|
417
508
|
continue
|
|
418
509
|
|
|
419
510
|
try:
|
|
420
|
-
|
|
511
|
+
body_for_tier = self._prepare_body_for_tier(
|
|
512
|
+
body, tier, normalization, session_record=session_record
|
|
513
|
+
)
|
|
514
|
+
resp = await tier.vendor.send_message(body_for_tier, headers)
|
|
421
515
|
|
|
422
516
|
if resp.status_code < 400:
|
|
423
517
|
duration = int((time.monotonic() - start) * 1000)
|
|
@@ -134,18 +134,24 @@ class RequestRouter:
|
|
|
134
134
|
self,
|
|
135
135
|
body: dict[str, Any],
|
|
136
136
|
headers: dict[str, str],
|
|
137
|
+
normalization: Any = None,
|
|
137
138
|
) -> AsyncIterator[tuple[bytes, str]]:
|
|
138
139
|
"""路由流式请求,按优先级尝试各层级."""
|
|
139
|
-
async for chunk, vendor_name in self._executor.execute_stream(
|
|
140
|
+
async for chunk, vendor_name in self._executor.execute_stream(
|
|
141
|
+
body, headers, normalization=normalization
|
|
142
|
+
):
|
|
140
143
|
yield chunk, vendor_name
|
|
141
144
|
|
|
142
145
|
async def route_message(
|
|
143
146
|
self,
|
|
144
147
|
body: dict[str, Any],
|
|
145
148
|
headers: dict[str, str],
|
|
149
|
+
normalization: Any = None,
|
|
146
150
|
) -> Any:
|
|
147
151
|
"""路由非流式请求,按优先级尝试各层级."""
|
|
148
|
-
return await self._executor.execute_message(
|
|
152
|
+
return await self._executor.execute_message(
|
|
153
|
+
body, headers, normalization=normalization
|
|
154
|
+
)
|
|
149
155
|
|
|
150
156
|
# ── 生命周期 ───────────────────────────────────────────
|
|
151
157
|
|