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.
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/CHANGELOG.md +6 -13
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/PKG-INFO +2 -2
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/README.md +1 -1
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/zh-CN/README.md +1 -1
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/pyproject.toml +1 -1
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/cli/__init__.py +37 -3
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/config.default.yaml +6 -1
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/__init__.py +5 -1
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/router.py +60 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/routes.py +44 -2
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_app_routes.py +155 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/uv.lock +1 -1
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/.gitignore +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/AGENTS.md +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/CLAUDE.md +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/LICENSE +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/ci-cd.md +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/framework.md +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/docs/user-guide.md +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/request_normalizer.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/__init__.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_banner.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_compat.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_config_init.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_currency.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_mixins.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_token.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_pricing.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_request_normalizer.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_router_executor.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_schema.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_tier.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_time_range.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_types.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/tests/test_vendors.py +0 -0
- {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
|
-
|
|
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
|
-
-
|
|
11
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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**:
|
|
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**:
|
|
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)
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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": {
|
|
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() ->
|
|
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
|
-
|
|
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"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_proxy-0.2.1a3 → coding_proxy-0.2.2}/src/coding/proxy/vendors/copilot_token_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|