coding-proxy 0.2.1a3__tar.gz → 0.2.2__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.
Files changed (146) hide show
  1. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/CHANGELOG.md +6 -13
  2. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/PKG-INFO +2 -2
  3. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/README.md +1 -1
  4. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/zh-CN/README.md +1 -1
  5. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/pyproject.toml +1 -1
  6. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/cli/__init__.py +37 -3
  7. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/config.default.yaml +6 -1
  8. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/__init__.py +5 -1
  9. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/router.py +60 -0
  10. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/routes.py +44 -2
  11. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_app_routes.py +155 -0
  12. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/uv.lock +1 -1
  13. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.github/workflows/ci.yml +0 -0
  14. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.github/workflows/coverage.yml +0 -0
  15. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.github/workflows/release.yml +0 -0
  16. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.gitignore +0 -0
  17. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/AGENTS.md +0 -0
  18. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/CLAUDE.md +0 -0
  19. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/LICENSE +0 -0
  20. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/ci-cd.md +0 -0
  21. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/framework.md +0 -0
  22. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/user-guide.md +0 -0
  23. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/__init__.py +0 -0
  24. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/__init__.py +0 -0
  25. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/__main__.py +0 -0
  26. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/__init__.py +0 -0
  27. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/__init__.py +0 -0
  28. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/base.py +0 -0
  29. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/github.py +0 -0
  30. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/google.py +0 -0
  31. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/runtime.py +0 -0
  32. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/store.py +0 -0
  33. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/cli/auth_commands.py +0 -0
  34. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/cli/banner.py +0 -0
  35. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/compat/__init__.py +0 -0
  36. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/compat/canonical.py +0 -0
  37. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/compat/session_store.py +0 -0
  38. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/__init__.py +0 -0
  39. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/auth_schema.py +0 -0
  40. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/loader.py +0 -0
  41. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/resiliency.py +0 -0
  42. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/routing.py +0 -0
  43. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/schema.py +0 -0
  44. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/server.py +0 -0
  45. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/vendors.py +0 -0
  46. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/__init__.py +0 -0
  47. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  48. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  49. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  50. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  51. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  52. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/db.py +0 -0
  53. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/formatters.py +0 -0
  54. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/stats.py +0 -0
  55. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/__init__.py +0 -0
  56. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/auth.py +0 -0
  57. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/compat.py +0 -0
  58. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/constants.py +0 -0
  59. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/pricing.py +0 -0
  60. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/token.py +0 -0
  61. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/vendor.py +0 -0
  62. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/pricing.py +0 -0
  63. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/__init__.py +0 -0
  64. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  65. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/error_classifier.py +0 -0
  66. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/executor.py +0 -0
  67. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/model_mapper.py +0 -0
  68. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/quota_guard.py +0 -0
  69. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/rate_limit.py +0 -0
  70. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/retry.py +0 -0
  71. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/session_manager.py +0 -0
  72. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/tier.py +0 -0
  73. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/usage_parser.py +0 -0
  74. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/usage_recorder.py +0 -0
  75. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/__init__.py +0 -0
  76. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/app.py +0 -0
  77. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/factory.py +0 -0
  78. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/request_normalizer.py +0 -0
  79. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/responses.py +0 -0
  80. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/streaming/__init__.py +0 -0
  81. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  82. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/__init__.py +0 -0
  83. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/alibaba.py +0 -0
  84. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/anthropic.py +0 -0
  85. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/antigravity.py +0 -0
  86. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/base.py +0 -0
  87. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot.py +0 -0
  88. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_models.py +0 -0
  89. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  90. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  91. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/doubao.py +0 -0
  92. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/kimi.py +0 -0
  93. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/minimax.py +0 -0
  94. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/mixins.py +0 -0
  95. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  96. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/token_manager.py +0 -0
  97. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/xiaomi.py +0 -0
  98. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/zhipu.py +0 -0
  99. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/__init__.py +0 -0
  100. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_antigravity.py +0 -0
  101. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_auto_login.py +0 -0
  102. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_banner.py +0 -0
  103. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_circuit_breaker.py +0 -0
  104. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_cli_usage.py +0 -0
  105. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_compat.py +0 -0
  106. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_config_init.py +0 -0
  107. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_config_loader.py +0 -0
  108. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_convert_request.py +0 -0
  109. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_convert_response.py +0 -0
  110. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_convert_sse.py +0 -0
  111. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot.py +0 -0
  112. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_convert_request.py +0 -0
  113. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_convert_response.py +0 -0
  114. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_models.py +0 -0
  115. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_urls.py +0 -0
  116. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_currency.py +0 -0
  117. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_error_classifier.py +0 -0
  118. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_logging_dual_write.py +0 -0
  119. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_mixins.py +0 -0
  120. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_auth.py +0 -0
  121. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_compat.py +0 -0
  122. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_constants.py +0 -0
  123. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_mapper.py +0 -0
  124. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_pricing.py +0 -0
  125. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_token.py +0 -0
  126. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_vendor.py +0 -0
  127. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_native_vendors.py +0 -0
  128. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_parse_usage.py +0 -0
  129. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_pricing.py +0 -0
  130. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_quota_guard.py +0 -0
  131. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_rate_limit.py +0 -0
  132. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_request_normalizer.py +0 -0
  133. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_router_chain.py +0 -0
  134. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_router_executor.py +0 -0
  135. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_runtime_reauth.py +0 -0
  136. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_schema.py +0 -0
  137. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_streaming_anthropic_compat.py +0 -0
  138. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_tier.py +0 -0
  139. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_tiers_config.py +0 -0
  140. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_time_range.py +0 -0
  141. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_token_logger.py +0 -0
  142. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_token_manager.py +0 -0
  143. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_types.py +0 -0
  144. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_vendor_streaming.py +0 -0
  145. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_vendors.py +0 -0
  146. {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_zhipu.py +0 -0
@@ -4,22 +4,15 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- - style(lint): 移除未使用变量 `old_base_url` 并合并重复的 `antigravity` 导入块;
8
- ## [v0.2.1](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.1a1) — 2026-04-11
7
+ ## [v0.2.2](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.2) 2026-04-13
9
8
 
10
- - fix(antigravity): 修复 Google OAuth token 刷新后 scope 校验过严导致 403 的问题;
11
- - refactor(request-normalizer): 移除跨供应商 tool_result 重定位死代码,docstring 对齐实际剥离行为;
9
+ - feat(reset): CLI reset 命令新增 -v/--vendor 参数,支持运行时 N-tier 链路重排序(逗号分隔的 vendor 列表);
10
+ - fix(logging): 修复 uvicorn.error 日志在文件中重复打印的问题;
12
11
 
12
+ ## [v0.2.1](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.1) — 2026-04-11
13
13
 
14
- - **请求规范化死代码清理**:移除 `request_normalizer` 中永远不可达的 `tool_result` 重定位逻辑(Phase 1 剥离已覆盖全部场景,Phase 2 重定位因方向错误无法启用);更新 docstring 使之与实际行为(剥离)一致;
15
-
16
- ### 🐛 Bug 修复
17
-
18
- - **fix(antigravity)**: 新增 Google Cloud Code **v1internal 协议支持 + project_id 自动发现**,彻底解决 `ACCESS_TOKEN_SCOPE_INSUFFICIENT` (403) 问题。
19
- - **根因**:此前调用标准 Generative Language API (`generativelanguage.googleapis.com`),该端点对 OAuth scope 校验严格;参考项目 Antigravity-Manager 实际使用的是 Cloud Code v1internal 内部 API (`cloudcode-pa.googleapis.com/v1internal`),接受相同凭证但协议格式不同
20
- - **修复(v1internal 协议)**:新增 `project_id` 配置字段 + v1internal 请求信封包装 + 客户端指纹 Headers + 端点 URL 适配
21
- - **修复(自动发现)**:利用已有的 `cloud-platform` OAuth scope 通过 Cloud Resource Manager API 自动发现用户的 GCP `project_id`,首次请求时零配置自动切换至 v1internal 模式——开箱即用,无需手动配置
22
- - **附带改进**:`_acquire()` scope 校验保持 warning 降级;`_mark_scope_error_if_needed()` 增强诊断日志;`get_diagnostics()` 暴露发现状态
14
+ - feat(logging): 实现日志双写(控制台 + 本地文件),日志文件支持 5MB 自动轮转及 gzip 压缩备份;ModelCall 日志降级为 DEBUG 级别;
15
+ - feat(circuit-breaker): 补全熔断器状态转换日志的 vendor 上下文信息;
23
16
 
24
17
  ## [v0.2.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.2.0) — 2026-04-09
25
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.2.1a3
3
+ Version: 0.2.2
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,7 +56,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
56
56
 
57
57
  ## 🌟 Core Features
58
58
 
59
- - **⛓️ N-tier Chained Failover**: Automatically downgrades from official Claude Plans, gracefully falling back to GitHub Copilot, then Google Antigravity, with Zhipu GLM acting as the ultimate safety net.
59
+ - **⛓️ 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
60
  - **🛡️ 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
61
  - **👻 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.
62
62
  - **🔄 Universal Alchemy (Formats & Models)**: Native support for two-way request/streaming (SSE) translations between Anthropic ←→ Gemini. Plus, auto/DIY model name mapping (e.g., effortlessly morphing `claude-*` into `glm-*`).
@@ -29,7 +29,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
29
29
 
30
30
  ## 🌟 Core Features
31
31
 
32
- - **⛓️ N-tier Chained Failover**: Automatically downgrades from official Claude Plans, gracefully falling back to GitHub Copilot, then Google Antigravity, with Zhipu GLM acting as the ultimate safety net.
32
+ - **⛓️ 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
33
  - **🛡️ 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
34
  - **👻 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.
35
35
  - **🔄 Universal Alchemy (Formats & Models)**: Native support for two-way request/streaming (SSE) translations between Anthropic ←→ Gemini. Plus, auto/DIY model name mapping (e.g., effortlessly morphing `claude-*` into `glm-*`).
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## 🌟 核心特性 (Core Features)
31
31
 
32
- - **⛓️ N-tier 链式故障转移 (Failover)**:自动从 Claude 官方 Plans 依次降级至 GitHub Copilot、Google Antigravity,最后由智谱 GLM 兜底。
32
+ - **⛓️ N-tier 链式故障转移 (Failover)**:自主降序序列,支持 Claude 官方 Plans,以及 GitHub Copilot、智谱、MiniMax、阿里千问、小米、Kimi、豆包等的 Coding Plan。
33
33
  - **🛡️ 智能弹性与容灾守卫**:每个供应商节点独立配备 **熔断器 (Circuit Breaker)** 与 **配额守卫 (Quota Guard)**,防雪崩、主动避险。
34
34
  - **👻 透明无感代理机制**:对客户端 **100% 透明**!无需修改任何代码,仅需一行配置覆盖 `ANTHROPIC_BASE_URL` 即可接入。
35
35
  - **🔄 跨模型与全格式转换**:原生支持 Anthropic ←→ Gemini 的请求与流式响应(SSE)双向转换,并支持自动/自助映射模型名称(如 `claude-*` 至 `glm-*`)。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.2.1a3"
3
+ version = "0.2.2"
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"
@@ -216,16 +216,50 @@ async def _run_usage(
216
216
  @app.command()
217
217
  def reset(
218
218
  port: int = typer.Option(8046, "--port", "-p", help="代理服务端口"),
219
+ vendor: str | None = typer.Option(
220
+ None,
221
+ "--vendor",
222
+ "-v",
223
+ help="提升/重排序 vendor 优先级(单个或逗号分隔多个)",
224
+ ),
219
225
  ) -> None:
220
- """重置所有层级的熔断器和配额守卫(恢复使用最高优先级供应商)."""
226
+ """重置所有层级的熔断器和配额守卫.
227
+
228
+ 可通过 -v 指定运行时 N-tier 链路重排序:
229
+
230
+ \b
231
+ -v zhipu 提升 zhipu 到最高优先级
232
+ -v zhipu,anthropic 替换整个 N-tier 链路顺序
233
+ """
221
234
  import httpx
222
235
 
236
+ # 构建请求 body
237
+ json_body: dict | None = None
238
+ if vendor:
239
+ parts = [v.strip() for v in vendor.split(",") if v.strip()]
240
+ if parts:
241
+ json_body = {"vendors": parts}
242
+
223
243
  try:
224
- resp = httpx.post(f"http://127.0.0.1:{port}/api/reset", timeout=5)
244
+ resp = httpx.post(
245
+ f"http://127.0.0.1:{port}/api/reset",
246
+ json=json_body,
247
+ timeout=5,
248
+ )
225
249
  if resp.status_code == 200:
250
+ data = resp.json()
226
251
  console.print("[green]所有层级的熔断器和配额守卫已重置[/green]")
252
+ tier_order = data.get("tier_order")
253
+ if tier_order:
254
+ order_str = " → ".join(tier_order)
255
+ console.print(f"[cyan]当前链路顺序:[/] {order_str}")
227
256
  else:
228
- console.print(f"[red]重置失败: {resp.status_code}[/red]")
257
+ try:
258
+ err = resp.json()
259
+ msg = err.get("error", {}).get("message", resp.text)
260
+ except Exception:
261
+ msg = resp.text
262
+ console.print(f"[red]重置失败: {msg}[/red]")
229
263
  except httpx.ConnectError:
230
264
  console.print("[red]代理服务未运行[/red]")
231
265
 
@@ -111,7 +111,7 @@ vendors:
111
111
  # 不配置 circuit_breaker → 自动成为终端层,不触发向下故障转移
112
112
  circuit_breaker:
113
113
  failure_threshold: 3
114
- recovery_timeout_seconds: 300
114
+ recovery_timeout_seconds: 30
115
115
  success_threshold: 2
116
116
  quota_guard:
117
117
  enabled: true # 启用后按 Premium Requests 配额管理
@@ -421,6 +421,11 @@ pricing:
421
421
  input_cost_per_mtok: ¥0.80
422
422
  output_cost_per_mtok: ¥2.00
423
423
  cache_read_cost_per_mtok: ¥0.16
424
+ - vendor: zhipu
425
+ model: glm-4.7 # 待区分长短上下文定价
426
+ input_cost_per_mtok: ¥2.00
427
+ output_cost_per_mtok: ¥8.00
428
+ cache_read_cost_per_mtok: ¥0.40
424
429
  - vendor: zhipu
425
430
  model: glm-5v-turbo # 待区分长短上下文定价
426
431
  input_cost_per_mtok: ¥5.00
@@ -118,7 +118,11 @@ def build_log_config(
118
118
  },
119
119
  "loggers": {
120
120
  "uvicorn": {"handlers": ["default"], "level": level, "propagate": False},
121
- "uvicorn.error": {"level": level},
121
+ "uvicorn.error": {
122
+ "handlers": ["default"],
123
+ "level": level,
124
+ "propagate": False,
125
+ },
122
126
  "uvicorn.access": {
123
127
  "handlers": ["access"],
124
128
  "level": "INFO",
@@ -68,6 +68,66 @@ class RequestRouter:
68
68
  """当前活跃供应商名称(由 Executor 在成功响应时写入)."""
69
69
  return self._active_vendor_name
70
70
 
71
+ # ── 运行时 N-tier 链路重排序 ─────────────────────────────
72
+
73
+ def get_vendor_names(self) -> list[str]:
74
+ """返回当前 tiers 的供应商名称列表(按优先级顺序)."""
75
+ return [t.name for t in self._tiers]
76
+
77
+ def reorder_tiers(self, vendor_names: list[str]) -> None:
78
+ """原地重排序 N-tier 链路.
79
+
80
+ 使用切片赋值保持列表引用同一性,使 ``_RouteExecutor`` 立即可见。
81
+
82
+ Args:
83
+ vendor_names: 新的供应商名称顺序(必须包含所有当前 tier)。
84
+
85
+ Raises:
86
+ ValueError: 名称不存在、有重复、或未覆盖所有 tier。
87
+ """
88
+ name_to_tier = {t.name: t for t in self._tiers}
89
+ current_names = set(name_to_tier)
90
+
91
+ # 校验:重复
92
+ if len(vendor_names) != len(set(vendor_names)):
93
+ seen: set[str] = set()
94
+ dups = [n for n in vendor_names if n in seen or seen.add(n)] # type: ignore[func-returns-value]
95
+ raise ValueError(f"vendor 名称重复: {', '.join(dups)}")
96
+
97
+ # 校验:名称存在性
98
+ unknown = [n for n in vendor_names if n not in current_names]
99
+ if unknown:
100
+ raise ValueError(
101
+ f"未知 vendor: {', '.join(unknown)}; "
102
+ f"可用: {', '.join(sorted(current_names))}"
103
+ )
104
+
105
+ # 校验:全量覆盖
106
+ provided = set(vendor_names)
107
+ if provided != current_names:
108
+ missing = current_names - provided
109
+ raise ValueError(f"缺少 vendor: {', '.join(sorted(missing))}")
110
+
111
+ self._tiers[:] = [name_to_tier[n] for n in vendor_names]
112
+
113
+ def promote_vendor(self, vendor_name: str) -> None:
114
+ """将指定 vendor 提升至最高优先级,其余保持相对顺序.
115
+
116
+ Args:
117
+ vendor_name: 要提升的供应商名称。
118
+
119
+ Raises:
120
+ ValueError: 名称不存在。
121
+ """
122
+ current_names = self.get_vendor_names()
123
+ if vendor_name not in current_names:
124
+ available = sorted(t.name for t in self._tiers)
125
+ raise ValueError(
126
+ f"未知 vendor: {vendor_name}; 可用: {', '.join(available)}"
127
+ )
128
+ new_order = [vendor_name] + [n for n in current_names if n != vendor_name]
129
+ self.reorder_tiers(new_order)
130
+
71
131
  # ── 公开路由接口(委托给 _RouteExecutor)───────────────
72
132
 
73
133
  async def route_stream(
@@ -261,7 +261,40 @@ def register_admin_routes(app: Any, router: Any) -> None:
261
261
  """注册管理操作路由(重置等)."""
262
262
 
263
263
  @app.post("/api/reset")
264
- async def reset_circuit() -> dict:
264
+ async def reset_circuit(request: Request) -> Response:
265
+ """重置所有层级的熔断器/配额守卫/rate limit.
266
+
267
+ 可选 JSON body ``{"vendors": ["v1", "v2", ...]}`` 支持运行时重排序:
268
+ - 单个 vendor → 提升至最高优先级,其余保持相对顺序
269
+ - 多个 vendor → 替换整个 N-tier 链路顺序(需覆盖所有 vendor)
270
+ """
271
+ # 解析可选 body
272
+ vendor_names: list[str] | None = None
273
+ try:
274
+ body = await request.json()
275
+ if isinstance(body, dict):
276
+ raw = body.get("vendors")
277
+ if isinstance(raw, list) and raw:
278
+ vendor_names = [str(v) for v in raw]
279
+ except Exception:
280
+ # 无 body 或非 JSON → 仅 reset(向后兼容)
281
+ pass
282
+
283
+ # 重排序(如果指定)
284
+ if vendor_names is not None:
285
+ try:
286
+ if len(vendor_names) == 1:
287
+ router.promote_vendor(vendor_names[0])
288
+ else:
289
+ router.reorder_tiers(vendor_names)
290
+ except ValueError as exc:
291
+ return json_error_response(
292
+ 400,
293
+ error_type="invalid_request_error",
294
+ message=str(exc),
295
+ )
296
+
297
+ # 全量 reset
265
298
  for tier in router.tiers:
266
299
  if tier.circuit_breaker:
267
300
  tier.circuit_breaker.reset()
@@ -270,7 +303,16 @@ def register_admin_routes(app: Any, router: Any) -> None:
270
303
  if tier.weekly_quota_guard:
271
304
  tier.weekly_quota_guard.reset()
272
305
  tier.reset_rate_limit()
273
- return {"status": "ok"}
306
+
307
+ result: dict[str, Any] = {"status": "ok"}
308
+ if vendor_names is not None:
309
+ result["tier_order"] = router.get_vendor_names()
310
+
311
+ return Response(
312
+ content=json.dumps(result, ensure_ascii=False).encode(),
313
+ status_code=200,
314
+ media_type="application/json",
315
+ )
274
316
 
275
317
 
276
318
  def register_reauth_routes(app: Any, reauth_coordinator: Any) -> None:
@@ -868,3 +868,158 @@ def test_vendor_500_passthrough_preserves_raw_body():
868
868
 
869
869
  assert resp.status_code == 500
870
870
  assert resp.content == original_body
871
+
872
+
873
+ # ── /api/reset 重排序测试 ────────────────────────────────────────
874
+
875
+
876
+ def _make_reorder_app() -> tuple:
877
+ """创建包含 anthropic + zhipu + copilot 三层的测试应用."""
878
+ config = ProxyConfig(
879
+ tiers=[
880
+ {
881
+ "vendor": "anthropic",
882
+ "enabled": True,
883
+ "circuit_breaker": {"failure_threshold": 3},
884
+ },
885
+ {"vendor": "zhipu", "enabled": True, "api_key": "sk-test"},
886
+ {"vendor": "copilot", "enabled": True},
887
+ ],
888
+ database={"path": "/tmp/test-coding-proxy-reorder.db"},
889
+ )
890
+ app = create_app(config)
891
+
892
+ async def route_ok(body, headers):
893
+ return VendorResponse(
894
+ status_code=200, raw_body=b"{}", usage=UsageInfo(input_tokens=1)
895
+ )
896
+
897
+ for tier in app.state.router.tiers:
898
+ tier.vendor.send_message = route_ok
899
+
900
+ return app
901
+
902
+
903
+ def test_reset_promote_single_vendor():
904
+ """单 vendor → promote_vendor:将该 vendor 提升至首位,其余保持相对顺序."""
905
+ app = _make_reorder_app()
906
+ with TestClient(app) as client:
907
+ resp = client.post("/api/reset", json={"vendors": ["zhipu"]})
908
+ assert resp.status_code == 200
909
+ data = resp.json()
910
+ assert data["tier_order"] == ["zhipu", "anthropic", "copilot"]
911
+
912
+ # 验证路由器内部状态一致
913
+ assert app.state.router.get_vendor_names() == ["zhipu", "anthropic", "copilot"]
914
+
915
+
916
+ def test_reset_reorder_full_chain():
917
+ """多 vendor → reorder_tiers:精确匹配指定顺序."""
918
+ app = _make_reorder_app()
919
+ with TestClient(app) as client:
920
+ resp = client.post(
921
+ "/api/reset", json={"vendors": ["copilot", "anthropic", "zhipu"]}
922
+ )
923
+ assert resp.status_code == 200
924
+ data = resp.json()
925
+ assert data["tier_order"] == ["copilot", "anthropic", "zhipu"]
926
+ assert app.state.router.get_vendor_names() == [
927
+ "copilot",
928
+ "anthropic",
929
+ "zhipu",
930
+ ]
931
+
932
+
933
+ def test_reset_no_body_backward_compatible():
934
+ """无 body → 仅 reset,不返回 tier_order(向后兼容)."""
935
+ app = _make_reorder_app()
936
+ with TestClient(app) as client:
937
+ resp = client.post("/api/reset")
938
+ assert resp.status_code == 200
939
+ data = resp.json()
940
+ assert "tier_order" not in data
941
+ assert data["status"] == "ok"
942
+ # 顺序不变
943
+ assert app.state.router.get_vendor_names() == [
944
+ "anthropic",
945
+ "zhipu",
946
+ "copilot",
947
+ ]
948
+
949
+
950
+ def test_reset_unknown_vendor_returns_400():
951
+ """未知 vendor 名称 → 400 错误."""
952
+ app = _make_reorder_app()
953
+ with TestClient(app) as client:
954
+ resp = client.post("/api/reset", json={"vendors": ["nonexist"]})
955
+ assert resp.status_code == 400
956
+ err = resp.json()["error"]
957
+ assert "未知 vendor" in err["message"]
958
+
959
+
960
+ def test_reset_duplicate_vendor_returns_400():
961
+ """重复 vendor 名称 → 400 错误."""
962
+ app = _make_reorder_app()
963
+ with TestClient(app) as client:
964
+ resp = client.post(
965
+ "/api/reset", json={"vendors": ["anthropic", "anthropic", "zhipu"]}
966
+ )
967
+ assert resp.status_code == 400
968
+ err = resp.json()["error"]
969
+ assert "重复" in err["message"]
970
+
971
+
972
+ def test_reset_incomplete_vendor_list_returns_400():
973
+ """不完整的 vendor 列表(缺少现有 tier)→ 400 错误."""
974
+ app = _make_reorder_app()
975
+ with TestClient(app) as client:
976
+ resp = client.post("/api/reset", json={"vendors": ["anthropic", "zhipu"]})
977
+ assert resp.status_code == 400
978
+ err = resp.json()["error"]
979
+ assert "缺少 vendor" in err["message"]
980
+
981
+
982
+ def test_reset_reorder_also_resets_circuit_breaker_and_rate_limit():
983
+ """重排序同时执行全量 reset(熔断器/配额守卫/rate limit 均被重置)."""
984
+ app = _make_reorder_app()
985
+ router = app.state.router
986
+
987
+ # 手动触发熔断器失败并设置 rate limit
988
+ router.tiers[0].record_failure(retry_after_seconds=300)
989
+ router.tiers[0]._rate_limit_deadline = 999999.0
990
+ assert not router.tiers[0].can_execute()
991
+
992
+ with TestClient(app) as client:
993
+ resp = client.post("/api/reset", json={"vendors": ["zhipu"]})
994
+ assert resp.status_code == 200
995
+
996
+ # 重排序后原 anthropic 仍是 tier 成员,但熔断器/rate limit 已被重置
997
+ anthropic_tier = next(t for t in router.tiers if t.name == "zhipu")
998
+ assert anthropic_tier.can_execute()
999
+ assert not anthropic_tier.is_rate_limited
1000
+
1001
+
1002
+ def test_reorder_tiers_shared_reference():
1003
+ """验证 reorder_tiers 使用切片赋值,Executor 立即可见."""
1004
+ from coding.proxy.routing.router import RequestRouter
1005
+ from coding.proxy.routing.tier import VendorTier
1006
+
1007
+ t1 = VendorTier(vendor=MagicMock())
1008
+ t1.vendor.get_name.return_value = "a"
1009
+ t2 = VendorTier(vendor=MagicMock())
1010
+ t2.vendor.get_name.return_value = "b"
1011
+ t3 = VendorTier(vendor=MagicMock())
1012
+ t3.vendor.get_name.return_value = "c"
1013
+
1014
+ router = RequestRouter([t1, t2, t3])
1015
+ executor_tiers = router._executor._tiers
1016
+
1017
+ # 验证共享引用
1018
+ assert executor_tiers is router._tiers
1019
+
1020
+ # 重排序
1021
+ router.reorder_tiers(["c", "a", "b"])
1022
+
1023
+ # Executor 的列表也改变了(因为是同一个对象)
1024
+ assert [t.name for t in executor_tiers] == ["c", "a", "b"]
1025
+ assert router.get_vendor_names() == ["c", "a", "b"]
@@ -65,7 +65,7 @@ wheels = [
65
65
 
66
66
  [[package]]
67
67
  name = "coding-proxy"
68
- version = "0.2.1a3"
68
+ version = "0.2.2"
69
69
  source = { editable = "." }
70
70
  dependencies = [
71
71
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes