coding-proxy 0.3.1a3__tar.gz → 0.3.1a4__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.3.1a3 → coding_proxy-0.3.1a4}/PKG-INFO +1 -1
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/pyproject.toml +1 -1
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/anthropic_to_openai.py +23 -13
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/executor.py +25 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/usage_parser.py +6 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_copilot_convert_request.py +129 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_parse_usage.py +28 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_parse_usage_gemini.py +16 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_router_executor.py +180 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/uv.lock +1 -1
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/.gitignore +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/AGENTS.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/CHANGELOG.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/LICENSE +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/README.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/assets/dashboard-v0.2.4.png +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/issue.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_types.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_vendors.py +0 -0
- {coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/tests/test_zhipu.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1a4
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1a4"
|
|
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"
|
{coding_proxy-0.3.1a3 → coding_proxy-0.3.1a4}/src/coding/proxy/convert/anthropic_to_openai.py
RENAMED
|
@@ -284,24 +284,34 @@ def _translate_assistant_message(message: dict[str, Any]) -> list[dict[str, Any]
|
|
|
284
284
|
final_text_parts = text_parts
|
|
285
285
|
|
|
286
286
|
if tool_uses:
|
|
287
|
+
tool_calls: list[dict[str, Any]] = []
|
|
288
|
+
for block in tool_uses:
|
|
289
|
+
raw_input = block.get("input")
|
|
290
|
+
if not isinstance(raw_input, dict):
|
|
291
|
+
logger.debug(
|
|
292
|
+
"copilot: tool_use id=%s name=%s has non-dict input (type=%s), "
|
|
293
|
+
"defaulting to empty dict",
|
|
294
|
+
block.get("id", ""),
|
|
295
|
+
block.get("name", ""),
|
|
296
|
+
type(raw_input).__name__,
|
|
297
|
+
)
|
|
298
|
+
raw_input = {}
|
|
299
|
+
tool_calls.append(
|
|
300
|
+
{
|
|
301
|
+
"id": block.get("id", ""),
|
|
302
|
+
"type": "function",
|
|
303
|
+
"function": {
|
|
304
|
+
"name": block.get("name", ""),
|
|
305
|
+
"arguments": json.dumps(raw_input, ensure_ascii=False),
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
)
|
|
287
309
|
return [
|
|
288
310
|
{
|
|
289
311
|
"role": "assistant",
|
|
290
312
|
"content": "\n\n".join(part for part in final_text_parts if part)
|
|
291
313
|
or None,
|
|
292
|
-
"tool_calls":
|
|
293
|
-
{
|
|
294
|
-
"id": block.get("id", ""),
|
|
295
|
-
"type": "function",
|
|
296
|
-
"function": {
|
|
297
|
-
"name": block.get("name", ""),
|
|
298
|
-
"arguments": json.dumps(
|
|
299
|
-
block.get("input", {}), ensure_ascii=False
|
|
300
|
-
),
|
|
301
|
-
},
|
|
302
|
-
}
|
|
303
|
-
for block in tool_uses
|
|
304
|
-
],
|
|
314
|
+
"tool_calls": tool_calls,
|
|
305
315
|
}
|
|
306
316
|
]
|
|
307
317
|
|
|
@@ -135,6 +135,10 @@ def _is_likely_request_format_error(
|
|
|
135
135
|
# 非结构化响应体(非 JSON)
|
|
136
136
|
if not trimmed.startswith("{") and len(trimmed) < 200:
|
|
137
137
|
return True
|
|
138
|
+
# 结构化 JSON 400 但含 tool_call 格式错误码 → 格式不兼容
|
|
139
|
+
# (如 Copilot 返回 {"error":{"code":"invalid_tool_call_format",...}})
|
|
140
|
+
if "invalid_tool_call_format" in trimmed:
|
|
141
|
+
return True
|
|
138
142
|
return False
|
|
139
143
|
|
|
140
144
|
|
|
@@ -795,6 +799,27 @@ class _RouteExecutor:
|
|
|
795
799
|
tier.name,
|
|
796
800
|
)
|
|
797
801
|
|
|
802
|
+
# 补充检测:zhipu 500 — tool_result 块触发上游 AttributeError
|
|
803
|
+
# zhipu 后端在 tool_result 块上错误访问 .id 属性(应为 .tool_use_id),
|
|
804
|
+
# 此为已知的上游格式缺陷,应视为 format incompatibility 而非真实服务器故障。
|
|
805
|
+
if (
|
|
806
|
+
not semantic_rejection
|
|
807
|
+
and exc.response.status_code == 500
|
|
808
|
+
and request_body is not None
|
|
809
|
+
and _has_tool_results(request_body)
|
|
810
|
+
):
|
|
811
|
+
err_text = (exc.response.text or "")[:500]
|
|
812
|
+
if (
|
|
813
|
+
"'ClaudeContentBlockToolResult'" in err_text
|
|
814
|
+
and "has no attribute 'id'" in err_text
|
|
815
|
+
):
|
|
816
|
+
semantic_rejection = True
|
|
817
|
+
logger.warning(
|
|
818
|
+
"Tier %s zhipu tool_result format error (500), "
|
|
819
|
+
"treating as format incompatibility without circuit breaker penalty",
|
|
820
|
+
tier.name,
|
|
821
|
+
)
|
|
822
|
+
|
|
798
823
|
if semantic_rejection and not is_last:
|
|
799
824
|
return True, tier.name, exc
|
|
800
825
|
|
|
@@ -210,6 +210,9 @@ def parse_usage_from_chunk(
|
|
|
210
210
|
request_id=data.get("id"),
|
|
211
211
|
model_served=data.get("model"),
|
|
212
212
|
)
|
|
213
|
+
model_name = data.get("model")
|
|
214
|
+
if model_name:
|
|
215
|
+
usage["model_served"] = model_name
|
|
213
216
|
|
|
214
217
|
# Gemini SSE 格式: data.usageMetadata.{promptTokenCount, candidatesTokenCount, cachedContentTokenCount, thoughtsTokenCount, toolUsePromptTokenCount}
|
|
215
218
|
# Gemini 的流式响应在最后一帧(或每一帧)携带 usageMetadata;字段命名与
|
|
@@ -243,6 +246,9 @@ def parse_usage_from_chunk(
|
|
|
243
246
|
request_id=data.get("responseId") or data.get("id"),
|
|
244
247
|
model_served=data.get("modelVersion") or data.get("model"),
|
|
245
248
|
)
|
|
249
|
+
model_name = data.get("modelVersion") or data.get("model")
|
|
250
|
+
if model_name:
|
|
251
|
+
usage["model_served"] = model_name
|
|
246
252
|
|
|
247
253
|
# request_id fallback (OpenAI 格式下 id 在顶层, Gemini 顶层为 responseId)
|
|
248
254
|
if not usage.get("request_id"):
|
|
@@ -472,3 +472,132 @@ def test_image_block_converted_to_image_url():
|
|
|
472
472
|
image_part = [p for p in user_msg["content"] if p.get("type") == "image_url"]
|
|
473
473
|
assert len(image_part) == 1
|
|
474
474
|
assert "data:image/png;base64,abc123" in image_part[0]["image_url"]["url"]
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# === Defensive tool_use.input serialization ===
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_tool_use_input_none_defaults_to_empty_dict():
|
|
481
|
+
"""input=None 应被降级为 {} 而非序列化为 'null'."""
|
|
482
|
+
body = {
|
|
483
|
+
"model": "claude-sonnet-4-20250514",
|
|
484
|
+
"messages": [
|
|
485
|
+
{
|
|
486
|
+
"role": "assistant",
|
|
487
|
+
"content": [
|
|
488
|
+
{
|
|
489
|
+
"type": "tool_use",
|
|
490
|
+
"id": "toolu_001",
|
|
491
|
+
"name": "read_file",
|
|
492
|
+
"input": None,
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
}
|
|
496
|
+
],
|
|
497
|
+
}
|
|
498
|
+
result = convert_request(body)
|
|
499
|
+
assistant_msgs = [m for m in result["messages"] if m["role"] == "assistant"]
|
|
500
|
+
assert len(assistant_msgs) == 1
|
|
501
|
+
assert "tool_calls" in assistant_msgs[0]
|
|
502
|
+
tc = assistant_msgs[0]["tool_calls"][0]
|
|
503
|
+
assert tc["function"]["arguments"] == "{}"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def test_tool_use_input_string_defaults_to_empty_dict():
|
|
507
|
+
"""input='some string' 应被降级为 {} 而非序列化为 '"some string"'."""
|
|
508
|
+
body = {
|
|
509
|
+
"model": "claude-sonnet-4-20250514",
|
|
510
|
+
"messages": [
|
|
511
|
+
{
|
|
512
|
+
"role": "assistant",
|
|
513
|
+
"content": [
|
|
514
|
+
{
|
|
515
|
+
"type": "tool_use",
|
|
516
|
+
"id": "toolu_002",
|
|
517
|
+
"name": "run_cmd",
|
|
518
|
+
"input": "not a dict",
|
|
519
|
+
}
|
|
520
|
+
],
|
|
521
|
+
}
|
|
522
|
+
],
|
|
523
|
+
}
|
|
524
|
+
result = convert_request(body)
|
|
525
|
+
assistant_msgs = [m for m in result["messages"] if m["role"] == "assistant"]
|
|
526
|
+
tc = assistant_msgs[0]["tool_calls"][0]
|
|
527
|
+
assert tc["function"]["arguments"] == "{}"
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def test_tool_use_input_missing_defaults_to_empty_dict():
|
|
531
|
+
"""input key 不存在时,block.get('input') 返回 None,应降级为 {}."""
|
|
532
|
+
body = {
|
|
533
|
+
"model": "claude-sonnet-4-20250514",
|
|
534
|
+
"messages": [
|
|
535
|
+
{
|
|
536
|
+
"role": "assistant",
|
|
537
|
+
"content": [
|
|
538
|
+
{
|
|
539
|
+
"type": "tool_use",
|
|
540
|
+
"id": "toolu_003",
|
|
541
|
+
"name": "search",
|
|
542
|
+
}
|
|
543
|
+
],
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
}
|
|
547
|
+
result = convert_request(body)
|
|
548
|
+
assistant_msgs = [m for m in result["messages"] if m["role"] == "assistant"]
|
|
549
|
+
tc = assistant_msgs[0]["tool_calls"][0]
|
|
550
|
+
assert tc["function"]["arguments"] == "{}"
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def test_tool_use_input_int_defaults_to_empty_dict():
|
|
554
|
+
"""input=42 应被降级为 {} 而非序列化为 '42'."""
|
|
555
|
+
body = {
|
|
556
|
+
"model": "claude-sonnet-4-20250514",
|
|
557
|
+
"messages": [
|
|
558
|
+
{
|
|
559
|
+
"role": "assistant",
|
|
560
|
+
"content": [
|
|
561
|
+
{
|
|
562
|
+
"type": "tool_use",
|
|
563
|
+
"id": "toolu_004",
|
|
564
|
+
"name": "calc",
|
|
565
|
+
"input": 42,
|
|
566
|
+
}
|
|
567
|
+
],
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
}
|
|
571
|
+
result = convert_request(body)
|
|
572
|
+
assistant_msgs = [m for m in result["messages"] if m["role"] == "assistant"]
|
|
573
|
+
tc = assistant_msgs[0]["tool_calls"][0]
|
|
574
|
+
assert tc["function"]["arguments"] == "{}"
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def test_tool_use_valid_dict_input_preserved():
|
|
578
|
+
"""正常 dict input 应保持原样."""
|
|
579
|
+
body = {
|
|
580
|
+
"model": "claude-sonnet-4-20250514",
|
|
581
|
+
"messages": [
|
|
582
|
+
{
|
|
583
|
+
"role": "assistant",
|
|
584
|
+
"content": [
|
|
585
|
+
{
|
|
586
|
+
"type": "tool_use",
|
|
587
|
+
"id": "toolu_005",
|
|
588
|
+
"name": "read_file",
|
|
589
|
+
"input": {"path": "/tmp/test.txt", "offset": 10},
|
|
590
|
+
}
|
|
591
|
+
],
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
}
|
|
595
|
+
result = convert_request(body)
|
|
596
|
+
assistant_msgs = [m for m in result["messages"] if m["role"] == "assistant"]
|
|
597
|
+
tc = assistant_msgs[0]["tool_calls"][0]
|
|
598
|
+
import json
|
|
599
|
+
|
|
600
|
+
assert json.loads(tc["function"]["arguments"]) == {
|
|
601
|
+
"path": "/tmp/test.txt",
|
|
602
|
+
"offset": 10,
|
|
603
|
+
}
|
|
@@ -117,6 +117,34 @@ def test_openai_zhipu_final_chunk():
|
|
|
117
117
|
assert usage["input_tokens"] == 200
|
|
118
118
|
assert usage["output_tokens"] == 80
|
|
119
119
|
assert usage["request_id"] == "chatcmpl-1"
|
|
120
|
+
assert usage["model_served"] == "glm-5.1"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_openai_final_chunk_with_model():
|
|
124
|
+
"""OpenAI 最终 chunk 有 model 字段时应提取到 model_served."""
|
|
125
|
+
usage: dict = {}
|
|
126
|
+
parse_usage_from_chunk(
|
|
127
|
+
_sse(
|
|
128
|
+
'{"id":"chatcmpl-2","model":"gpt-4o-2024-08-06",'
|
|
129
|
+
'"usage":{"prompt_tokens":50,"completion_tokens":20}}'
|
|
130
|
+
),
|
|
131
|
+
usage,
|
|
132
|
+
)
|
|
133
|
+
assert usage["input_tokens"] == 50
|
|
134
|
+
assert usage["output_tokens"] == 20
|
|
135
|
+
assert usage["model_served"] == "gpt-4o-2024-08-06"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_openai_final_chunk_without_model():
|
|
139
|
+
"""OpenAI 最终 chunk 无 model 字段时不应设置 model_served."""
|
|
140
|
+
usage: dict = {}
|
|
141
|
+
parse_usage_from_chunk(
|
|
142
|
+
_sse('{"id":"chatcmpl-3","usage":{"prompt_tokens":30,"completion_tokens":10}}'),
|
|
143
|
+
usage,
|
|
144
|
+
)
|
|
145
|
+
assert usage["input_tokens"] == 30
|
|
146
|
+
assert usage["output_tokens"] == 10
|
|
147
|
+
assert "model_served" not in usage
|
|
120
148
|
|
|
121
149
|
|
|
122
150
|
def test_openai_final_chunk_with_cache_tokens():
|
|
@@ -35,6 +35,7 @@ def test_gemini_usage_metadata_basic_fields():
|
|
|
35
35
|
assert usage["output_tokens"] == 42
|
|
36
36
|
assert usage.get("cache_read_tokens", 0) == 0
|
|
37
37
|
assert usage["request_id"] == "resp_abc"
|
|
38
|
+
assert usage["model_served"] == "gemini-2.0-flash"
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def test_gemini_usage_metadata_with_cached_content():
|
|
@@ -194,3 +195,18 @@ def test_gemini_partial_fields_ok():
|
|
|
194
195
|
)
|
|
195
196
|
assert usage["input_tokens"] == 77
|
|
196
197
|
assert "output_tokens" not in usage
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_gemini_model_fallback_to_data_model():
|
|
201
|
+
"""当 modelVersion 不存在时,应回退到 data.model."""
|
|
202
|
+
usage: dict = {}
|
|
203
|
+
parse_usage_from_chunk(
|
|
204
|
+
_sse(
|
|
205
|
+
'{"usageMetadata":{"promptTokenCount":80,"candidatesTokenCount":20},'
|
|
206
|
+
'"model":"gemini-1.5-flash"}'
|
|
207
|
+
),
|
|
208
|
+
usage,
|
|
209
|
+
)
|
|
210
|
+
assert usage["input_tokens"] == 80
|
|
211
|
+
assert usage["output_tokens"] == 20
|
|
212
|
+
assert usage["model_served"] == "gemini-1.5-flash"
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from unittest.mock import AsyncMock, MagicMock
|
|
13
13
|
|
|
14
|
+
import httpx
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
16
17
|
from coding.proxy.compat.canonical import (
|
|
@@ -1229,6 +1230,31 @@ class TestIsLikelyRequestFormatError:
|
|
|
1229
1230
|
is False
|
|
1230
1231
|
)
|
|
1231
1232
|
|
|
1233
|
+
def test_returns_true_for_invalid_tool_call_format(self):
|
|
1234
|
+
"""400 + 结构化 JSON 含 invalid_tool_call_format + tool_result → 格式不兼容."""
|
|
1235
|
+
json_body = '{"error":{"message":"Invalid JSON format in tool call arguments","code":"invalid_tool_call_format"}}'
|
|
1236
|
+
assert (
|
|
1237
|
+
_is_likely_request_format_error(
|
|
1238
|
+
status_code=400,
|
|
1239
|
+
error_body_text=json_body,
|
|
1240
|
+
body=self._body_with_tool_results(),
|
|
1241
|
+
)
|
|
1242
|
+
is True
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
def test_returns_false_for_invalid_tool_call_format_without_tool_results(self):
|
|
1246
|
+
"""invalid_tool_call_format 但无 tool_result → 不应匹配."""
|
|
1247
|
+
json_body = '{"error":{"message":"Invalid JSON format in tool call arguments","code":"invalid_tool_call_format"}}'
|
|
1248
|
+
body = {"model": "test", "messages": [{"role": "user", "content": "hi"}]}
|
|
1249
|
+
assert (
|
|
1250
|
+
_is_likely_request_format_error(
|
|
1251
|
+
status_code=400,
|
|
1252
|
+
error_body_text=json_body,
|
|
1253
|
+
body=body,
|
|
1254
|
+
)
|
|
1255
|
+
is False
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1232
1258
|
|
|
1233
1259
|
# ── TokenAcquireError 永久性凭证错误测试 ────────────────────
|
|
1234
1260
|
|
|
@@ -2101,3 +2127,157 @@ class TestPrepareBodyForTierSelfTransition:
|
|
|
2101
2127
|
b for b in result["messages"][0]["content"] if b.get("type") == "thinking"
|
|
2102
2128
|
)
|
|
2103
2129
|
assert thinking_block["signature"] == "zhipu_sig"
|
|
2130
|
+
|
|
2131
|
+
|
|
2132
|
+
# ── zhipu 500 tool_result 格式错误检测测试 ──────────────────────
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
class TestZhipu500ToolResultFormatError:
|
|
2136
|
+
"""验证 _handle_http_error 对 zhipu 500 'ClaudeContentBlockToolResult' 错误的处理.
|
|
2137
|
+
|
|
2138
|
+
zhipu 后端在 tool_result 块上错误访问 .id 属性(应为 .tool_use_id),
|
|
2139
|
+
此为已知的上游格式缺陷,应视为 format incompatibility(semantic rejection)
|
|
2140
|
+
而非真实服务器故障,不应计入熔断器。
|
|
2141
|
+
"""
|
|
2142
|
+
|
|
2143
|
+
@pytest.mark.asyncio
|
|
2144
|
+
async def test_zhipu_500_tool_result_error_triggers_semantic_rejection(self):
|
|
2145
|
+
"""zhipu 500 + 'ClaudeContentBlockToolResult' + tool_result → semantic rejection."""
|
|
2146
|
+
from coding.proxy.routing.circuit_breaker import CircuitBreaker
|
|
2147
|
+
|
|
2148
|
+
vendor = _mock_vendor("zhipu")
|
|
2149
|
+
error_body = (
|
|
2150
|
+
b'{"error":{"code":"500","message":"\'ClaudeContentBlockToolResult\' '
|
|
2151
|
+
b"object has no attribute 'id'\"}}"
|
|
2152
|
+
)
|
|
2153
|
+
response = httpx.Response(
|
|
2154
|
+
status_code=500,
|
|
2155
|
+
content=error_body,
|
|
2156
|
+
request=httpx.Request("POST", "https://example.com"),
|
|
2157
|
+
)
|
|
2158
|
+
exc = httpx.HTTPStatusError(
|
|
2159
|
+
"zhipu API error: 500", request=response.request, response=response
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2162
|
+
cb = CircuitBreaker(failure_threshold=3)
|
|
2163
|
+
tier = _make_tier(vendor, circuit_breaker=cb)
|
|
2164
|
+
exec_inst = _executor([tier, _make_tier(_mock_vendor("copilot"))])
|
|
2165
|
+
|
|
2166
|
+
body = {
|
|
2167
|
+
"model": "claude-opus-4-6",
|
|
2168
|
+
"messages": [
|
|
2169
|
+
{
|
|
2170
|
+
"role": "user",
|
|
2171
|
+
"content": [
|
|
2172
|
+
{
|
|
2173
|
+
"type": "tool_result",
|
|
2174
|
+
"tool_use_id": "tu_1",
|
|
2175
|
+
"content": "result",
|
|
2176
|
+
}
|
|
2177
|
+
],
|
|
2178
|
+
},
|
|
2179
|
+
],
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
should_continue, failed_name, _ = await exec_inst._handle_http_error(
|
|
2183
|
+
tier,
|
|
2184
|
+
exc,
|
|
2185
|
+
is_last=False,
|
|
2186
|
+
failed_tier_name=None,
|
|
2187
|
+
last_exc=None,
|
|
2188
|
+
is_stream=True,
|
|
2189
|
+
request_body=body,
|
|
2190
|
+
)
|
|
2191
|
+
|
|
2192
|
+
assert should_continue is True
|
|
2193
|
+
assert failed_name == "zhipu"
|
|
2194
|
+
# 不应计入熔断器
|
|
2195
|
+
assert cb.get_info()["failure_count"] == 0
|
|
2196
|
+
|
|
2197
|
+
@pytest.mark.asyncio
|
|
2198
|
+
async def test_zhipu_500_generic_error_records_failure(self):
|
|
2199
|
+
"""zhipu 500 但非 tool_result 格式错误 → 正常记录熔断器."""
|
|
2200
|
+
from coding.proxy.routing.circuit_breaker import CircuitBreaker
|
|
2201
|
+
|
|
2202
|
+
vendor = _mock_vendor("zhipu")
|
|
2203
|
+
error_body = b'{"error":{"code":"500","message":"Internal Server Error"}}'
|
|
2204
|
+
response = httpx.Response(
|
|
2205
|
+
status_code=500,
|
|
2206
|
+
content=error_body,
|
|
2207
|
+
request=httpx.Request("POST", "https://example.com"),
|
|
2208
|
+
)
|
|
2209
|
+
exc = httpx.HTTPStatusError(
|
|
2210
|
+
"zhipu API error: 500", request=response.request, response=response
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
cb = CircuitBreaker(failure_threshold=3)
|
|
2214
|
+
tier = _make_tier(vendor, circuit_breaker=cb)
|
|
2215
|
+
exec_inst = _executor([tier])
|
|
2216
|
+
|
|
2217
|
+
body = {
|
|
2218
|
+
"model": "claude-opus-4-6",
|
|
2219
|
+
"messages": [
|
|
2220
|
+
{
|
|
2221
|
+
"role": "user",
|
|
2222
|
+
"content": [
|
|
2223
|
+
{
|
|
2224
|
+
"type": "tool_result",
|
|
2225
|
+
"tool_use_id": "tu_1",
|
|
2226
|
+
"content": "result",
|
|
2227
|
+
}
|
|
2228
|
+
],
|
|
2229
|
+
},
|
|
2230
|
+
],
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
should_continue, _, _ = await exec_inst._handle_http_error(
|
|
2234
|
+
tier,
|
|
2235
|
+
exc,
|
|
2236
|
+
is_last=True,
|
|
2237
|
+
failed_tier_name=None,
|
|
2238
|
+
last_exc=None,
|
|
2239
|
+
is_stream=True,
|
|
2240
|
+
request_body=body,
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
# 非 last tier 时 should_continue=False,且应记录熔断器失败
|
|
2244
|
+
assert should_continue is False
|
|
2245
|
+
assert cb.get_info()["failure_count"] == 1
|
|
2246
|
+
|
|
2247
|
+
@pytest.mark.asyncio
|
|
2248
|
+
async def test_zhipu_500_tool_result_error_without_tool_results_body(self):
|
|
2249
|
+
"""zhipu 500 tool_result 错误但请求体无 tool_result → 不触发特殊处理."""
|
|
2250
|
+
from coding.proxy.routing.circuit_breaker import CircuitBreaker
|
|
2251
|
+
|
|
2252
|
+
vendor = _mock_vendor("zhipu")
|
|
2253
|
+
error_body = (
|
|
2254
|
+
b'{"error":{"code":"500","message":"\'ClaudeContentBlockToolResult\' '
|
|
2255
|
+
b"object has no attribute 'id'\"}}"
|
|
2256
|
+
)
|
|
2257
|
+
response = httpx.Response(
|
|
2258
|
+
status_code=500,
|
|
2259
|
+
content=error_body,
|
|
2260
|
+
request=httpx.Request("POST", "https://example.com"),
|
|
2261
|
+
)
|
|
2262
|
+
exc = httpx.HTTPStatusError(
|
|
2263
|
+
"zhipu API error: 500", request=response.request, response=response
|
|
2264
|
+
)
|
|
2265
|
+
|
|
2266
|
+
cb = CircuitBreaker(failure_threshold=3)
|
|
2267
|
+
tier = _make_tier(vendor, circuit_breaker=cb)
|
|
2268
|
+
exec_inst = _executor([tier])
|
|
2269
|
+
|
|
2270
|
+
body = {"model": "test", "messages": [{"role": "user", "content": "hello"}]}
|
|
2271
|
+
|
|
2272
|
+
should_continue, _, _ = await exec_inst._handle_http_error(
|
|
2273
|
+
tier,
|
|
2274
|
+
exc,
|
|
2275
|
+
is_last=True,
|
|
2276
|
+
failed_tier_name=None,
|
|
2277
|
+
last_exc=None,
|
|
2278
|
+
is_stream=True,
|
|
2279
|
+
request_body=body,
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
assert should_continue is False
|
|
2283
|
+
assert cb.get_info()["failure_count"] == 1
|
|
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
|