coding-proxy 0.5.1a7__tar.gz → 0.5.1a9__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.5.1a7 → coding_proxy-0.5.1a9}/PKG-INFO +1 -1
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/design-patterns.md +2 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/pyproject.toml +1 -1
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/config.default.yaml +45 -34
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/zhipu.py +42 -34
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_zhipu.py +181 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/uv.lock +1 -1
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.gitignore +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/AGENTS.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/CHANGELOG.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/CLAUDE.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/LICENSE +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/README.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/assets/model-calling-v0.5.0.png +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/browser-validation.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/issue.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/knowledge-map.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/reference-specifications.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/convert.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/routing.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/testing.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/framework.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/ops/ci-cd.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/user-guide.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/concurrency.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_banner.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_compat.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_concurrency_monitor.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_config_init.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_currency.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_executor_in_flight_tracking.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_mixins.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_token.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_pricing.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_router_executor.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_schema.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_tier.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_time_range.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_types.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_vendors.py +0 -0
- {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_zhipu_concurrency.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1a9
|
|
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
|
|
@@ -541,6 +541,8 @@ $$
|
|
|
541
541
|
|
|
542
542
|
> **参数默认值**:参见 [配置参考 -- RetryConfig](./config-reference.md#elastic-params)
|
|
543
543
|
|
|
544
|
+
**Vendor 级应用(Zhipu 429/529 内部退避重试)**:[`vendors/zhipu.py`](../../src/coding/proxy/vendors/zhipu.py) 复用 `RetryConfig` / `calculate_delay()`,在 `send_message` / `send_message_stream` 内对 **429 Rate Limit(限流)** 与 **529 Overloaded(并发过载)** 两类服务端瞬态过载信号做就地指数退避重试(共用 `_BACKOFF_RETRY_STATUS = {429, 529}` 作单一事实源,max=5、1s→2s→4s→8s、Full Jitter,优先尊重 server `retry-after`)。耗尽重试后将状态码原样返回,交由上层 `should_trigger_failover`(§3.6 VendorTier)与 [CircuitBreaker](#circuit-breaker) 处理,从而降低 failover 频率。
|
|
545
|
+
|
|
544
546
|
---
|
|
545
547
|
|
|
546
548
|
## 3.13 Rate Limit Deadline Tracking(速率限制截止追踪)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1a9"
|
|
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"
|
|
@@ -228,51 +228,51 @@ failover:
|
|
|
228
228
|
# === 模型映射 ===
|
|
229
229
|
model_mapping:
|
|
230
230
|
# GitHub Copilot 可用模型(通过 /api/copilot/models 诊断获取)
|
|
231
|
-
- pattern: "claude-sonnet-.*"
|
|
232
|
-
vendors: ["copilot"]
|
|
233
|
-
target: "claude-sonnet-4.6"
|
|
234
|
-
is_regex: true
|
|
235
231
|
- pattern: "claude-opus-.*"
|
|
236
232
|
vendors: ["copilot"]
|
|
237
233
|
target: "claude-opus-4.6"
|
|
238
234
|
is_regex: true
|
|
235
|
+
- pattern: "claude-sonnet-.*"
|
|
236
|
+
vendors: ["copilot"]
|
|
237
|
+
target: "claude-sonnet-4.6"
|
|
238
|
+
is_regex: true
|
|
239
239
|
- pattern: "claude-haiku-.*"
|
|
240
240
|
vendors: ["copilot"]
|
|
241
241
|
target: "claude-haiku-4.5"
|
|
242
242
|
is_regex: true
|
|
243
243
|
# Google Antigravity 模型映射
|
|
244
|
-
- pattern: "claude-sonnet-.*"
|
|
245
|
-
vendors: ["antigravity"]
|
|
246
|
-
target: "Claude Sonnet 4.6"
|
|
247
|
-
is_regex: true
|
|
248
244
|
- pattern: "claude-opus-.*"
|
|
249
245
|
vendors: ["antigravity"]
|
|
250
246
|
target: "Claude Opus 4.6 Thinking"
|
|
251
247
|
is_regex: true
|
|
248
|
+
- pattern: "claude-sonnet-.*"
|
|
249
|
+
vendors: ["antigravity"]
|
|
250
|
+
target: "Claude Sonnet 4.6"
|
|
251
|
+
is_regex: true
|
|
252
252
|
- pattern: "claude-haiku-.*"
|
|
253
253
|
vendors: ["antigravity"]
|
|
254
254
|
target: "Gemini 3 Flash"
|
|
255
255
|
is_regex: true
|
|
256
256
|
# 智谱 GLM 映射(官方原生 Anthropic 兼容端点)
|
|
257
257
|
# glm-5v-turbo 完整承载 Claude Code 满血能力(tool calling / thinking / 多模态)
|
|
258
|
-
- pattern: "claude-
|
|
258
|
+
- pattern: "claude-opus-.*"
|
|
259
259
|
vendors: ["zhipu"]
|
|
260
|
-
target: "glm-
|
|
260
|
+
target: "glm-5.2"
|
|
261
261
|
is_regex: true
|
|
262
|
-
- pattern: "claude-
|
|
262
|
+
- pattern: "claude-sonnet-.*"
|
|
263
263
|
vendors: ["zhipu"]
|
|
264
264
|
target: "glm-5.1"
|
|
265
265
|
is_regex: true
|
|
266
266
|
- pattern: "claude-haiku-.*"
|
|
267
267
|
vendors: ["zhipu"]
|
|
268
|
-
target: "glm-
|
|
268
|
+
target: "glm-5.1"
|
|
269
269
|
is_regex: true
|
|
270
270
|
# MiniMax 模型映射(原生 Anthropic 兼容端点)
|
|
271
|
-
- pattern: "claude-
|
|
271
|
+
- pattern: "claude-opus-.*"
|
|
272
272
|
vendors: ["minimax"]
|
|
273
273
|
target: "minimax-m2.7"
|
|
274
274
|
is_regex: true
|
|
275
|
-
- pattern: "claude-
|
|
275
|
+
- pattern: "claude-sonnet-.*"
|
|
276
276
|
vendors: ["minimax"]
|
|
277
277
|
target: "minimax-m2.7"
|
|
278
278
|
is_regex: true
|
|
@@ -281,11 +281,11 @@ model_mapping:
|
|
|
281
281
|
target: "minimax-m2.7"
|
|
282
282
|
is_regex: true
|
|
283
283
|
# 阿里 Qwen 模型映射(原生 Anthropic 兼容端点)
|
|
284
|
-
- pattern: "claude-
|
|
284
|
+
- pattern: "claude-opus-.*"
|
|
285
285
|
vendors: ["alibaba"]
|
|
286
286
|
target: "qwen3.6-plus"
|
|
287
287
|
is_regex: true
|
|
288
|
-
- pattern: "claude-
|
|
288
|
+
- pattern: "claude-sonnet-.*"
|
|
289
289
|
vendors: ["alibaba"]
|
|
290
290
|
target: "qwen3.6-plus"
|
|
291
291
|
is_regex: true
|
|
@@ -294,11 +294,11 @@ model_mapping:
|
|
|
294
294
|
target: "qwen3.6-plus"
|
|
295
295
|
is_regex: true
|
|
296
296
|
# 小米 MiMo 模型映射(原生 Anthropic 兼容端点)
|
|
297
|
-
- pattern: "claude-
|
|
297
|
+
- pattern: "claude-opus-.*"
|
|
298
298
|
vendors: ["xiaomi"]
|
|
299
299
|
target: "mimo-v2-pro"
|
|
300
300
|
is_regex: true
|
|
301
|
-
- pattern: "claude-
|
|
301
|
+
- pattern: "claude-sonnet-.*"
|
|
302
302
|
vendors: ["xiaomi"]
|
|
303
303
|
target: "mimo-v2-pro"
|
|
304
304
|
is_regex: true
|
|
@@ -307,11 +307,11 @@ model_mapping:
|
|
|
307
307
|
target: "mimo-v2-pro"
|
|
308
308
|
is_regex: true
|
|
309
309
|
# Kimi 模型映射(原生 Anthropic 兼容端点)
|
|
310
|
-
- pattern: "claude-
|
|
310
|
+
- pattern: "claude-opus-.*"
|
|
311
311
|
vendors: ["kimi"]
|
|
312
312
|
target: "kimi-k2.5"
|
|
313
313
|
is_regex: true
|
|
314
|
-
- pattern: "claude-
|
|
314
|
+
- pattern: "claude-sonnet-.*"
|
|
315
315
|
vendors: ["kimi"]
|
|
316
316
|
target: "kimi-k2.5"
|
|
317
317
|
is_regex: true
|
|
@@ -339,6 +339,12 @@ model_mapping:
|
|
|
339
339
|
# 未配置定价的模型在统计中 Cost 列显示 "-"
|
|
340
340
|
pricing:
|
|
341
341
|
# ── Anthropic Claude ──
|
|
342
|
+
- vendor: anthropic
|
|
343
|
+
model: claude-fable-5
|
|
344
|
+
input_cost_per_mtok: $10.0
|
|
345
|
+
output_cost_per_mtok: $50.0
|
|
346
|
+
cache_write_cost_per_mtok: $12.50
|
|
347
|
+
cache_read_cost_per_mtok: $1.00
|
|
342
348
|
- vendor: anthropic
|
|
343
349
|
model: claude-opus-4-8
|
|
344
350
|
input_cost_per_mtok: $5.0
|
|
@@ -527,20 +533,10 @@ pricing:
|
|
|
527
533
|
cache_read_cost_per_mtok: $0.05
|
|
528
534
|
# ── 智谱 GLM ──
|
|
529
535
|
- vendor: zhipu
|
|
530
|
-
model: glm-
|
|
531
|
-
input_cost_per_mtok: ¥
|
|
532
|
-
output_cost_per_mtok: ¥
|
|
533
|
-
cache_read_cost_per_mtok: ¥
|
|
534
|
-
- vendor: zhipu
|
|
535
|
-
model: glm-4.7 # 待区分长短上下文定价
|
|
536
|
-
input_cost_per_mtok: ¥2.00
|
|
537
|
-
output_cost_per_mtok: ¥8.00
|
|
538
|
-
cache_read_cost_per_mtok: ¥0.40
|
|
539
|
-
- vendor: zhipu
|
|
540
|
-
model: glm-5v-turbo # 待区分长短上下文定价
|
|
541
|
-
input_cost_per_mtok: ¥5.00
|
|
542
|
-
output_cost_per_mtok: ¥22.00
|
|
543
|
-
cache_read_cost_per_mtok: ¥1.20
|
|
536
|
+
model: glm-5.2 # 待区分长短上下文定价
|
|
537
|
+
input_cost_per_mtok: ¥6.00
|
|
538
|
+
output_cost_per_mtok: ¥24.00
|
|
539
|
+
cache_read_cost_per_mtok: ¥1.30
|
|
544
540
|
- vendor: zhipu
|
|
545
541
|
model: glm-5.1 # 待区分长短上下文定价
|
|
546
542
|
input_cost_per_mtok: ¥6.00
|
|
@@ -556,6 +552,21 @@ pricing:
|
|
|
556
552
|
input_cost_per_mtok: ¥4.00
|
|
557
553
|
output_cost_per_mtok: ¥18.00
|
|
558
554
|
cache_read_cost_per_mtok: ¥1.00
|
|
555
|
+
- vendor: zhipu
|
|
556
|
+
model: glm-5v-turbo # 待区分长短上下文定价
|
|
557
|
+
input_cost_per_mtok: ¥5.00
|
|
558
|
+
output_cost_per_mtok: ¥22.00
|
|
559
|
+
cache_read_cost_per_mtok: ¥1.20
|
|
560
|
+
- vendor: zhipu
|
|
561
|
+
model: glm-4.7 # 待区分长短上下文定价
|
|
562
|
+
input_cost_per_mtok: ¥2.00
|
|
563
|
+
output_cost_per_mtok: ¥8.00
|
|
564
|
+
cache_read_cost_per_mtok: ¥0.40
|
|
565
|
+
- vendor: zhipu
|
|
566
|
+
model: glm-4.5-air # 待区分长短上下文定价
|
|
567
|
+
input_cost_per_mtok: ¥0.80
|
|
568
|
+
output_cost_per_mtok: ¥2.00
|
|
569
|
+
cache_read_cost_per_mtok: ¥0.16
|
|
559
570
|
# ── MiniMax ──
|
|
560
571
|
- vendor: minimax
|
|
561
572
|
model: minimax-m2.7
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""智谱 GLM 供应商 — 原生 Anthropic 兼容端点代理(兼容转换 + 429 重试).
|
|
1
|
+
"""智谱 GLM 供应商 — 原生 Anthropic 兼容端点代理(兼容转换 + 429/529 重试).
|
|
2
2
|
|
|
3
3
|
官方端点 (https://open.bigmodel.cn/api/anthropic) 支持大部分
|
|
4
4
|
Anthropic Messages API 协议,本模块做以下适配:
|
|
@@ -13,7 +13,8 @@ Anthropic Messages API 协议,本模块做以下适配:
|
|
|
13
13
|
- reasoning_effort 参数:静默忽略
|
|
14
14
|
- metadata 字段:暂不处理(待进一步诊断确认兼容性)
|
|
15
15
|
|
|
16
|
-
额外提供 429
|
|
16
|
+
额外提供 429/529 专用重试挽回机制:
|
|
17
|
+
- 429 Rate Limit(限流)与 529 Overloaded(并发过载)共用同一退避策略
|
|
17
18
|
- max_attempt = 5(1 初始 + 4 重试)
|
|
18
19
|
- 指数退避 + Full Jitter(1s → 2s → 4s → 8s)
|
|
19
20
|
- 优先尊重 server retry-after header
|
|
@@ -56,14 +57,20 @@ _RATE_LIMIT_RETRY = RetryConfig(
|
|
|
56
57
|
jitter=True,
|
|
57
58
|
)
|
|
58
59
|
|
|
60
|
+
# 触发指数退避重试的瞬态状态码(单一事实源)
|
|
61
|
+
# 429 Rate Limit(限流);529 Overloaded(并发过载)
|
|
62
|
+
# 二者均为服务端瞬态过载信号,适用同一退避挽回策略。
|
|
63
|
+
_BACKOFF_RETRY_STATUS = frozenset({429, 529})
|
|
64
|
+
|
|
59
65
|
|
|
60
66
|
class ZhipuVendor(NativeAnthropicVendor):
|
|
61
|
-
"""智谱 GLM 原生 Anthropic 兼容端点供应商(薄透传 + 429 重试挽回).
|
|
67
|
+
"""智谱 GLM 原生 Anthropic 兼容端点供应商(薄透传 + 429/529 重试挽回).
|
|
62
68
|
|
|
63
69
|
通过官方 /api/anthropic 端点转发请求,
|
|
64
70
|
仅替换模型名和认证头,其余原样透传。
|
|
65
71
|
|
|
66
|
-
429 Rate Limit
|
|
72
|
+
429 Rate Limit(限流)/ 529 Overloaded(并发过载)时自动重试
|
|
73
|
+
(指数退避),降低 failover 频率。
|
|
67
74
|
并发限流由 BaseVendor._concurrency_controller 统一管控。
|
|
68
75
|
"""
|
|
69
76
|
|
|
@@ -124,24 +131,25 @@ class ZhipuVendor(NativeAnthropicVendor):
|
|
|
124
131
|
|
|
125
132
|
return body, new_headers
|
|
126
133
|
|
|
127
|
-
# ── 非流式:429 重试
|
|
134
|
+
# ── 非流式:429/529 重试 ────────────────────────────────
|
|
128
135
|
|
|
129
136
|
async def send_message(
|
|
130
137
|
self,
|
|
131
138
|
request_body: dict[str, Any],
|
|
132
139
|
headers: dict[str, str],
|
|
133
140
|
) -> VendorResponse:
|
|
134
|
-
"""非流式请求,429 时自动重试."""
|
|
141
|
+
"""非流式请求,429/529 时自动重试."""
|
|
135
142
|
max_attempts = self._rl_retry.max_attempts
|
|
136
143
|
|
|
137
144
|
for attempt in range(max_attempts):
|
|
138
145
|
resp = await super().send_message(request_body, headers)
|
|
139
|
-
if resp.status_code
|
|
146
|
+
if resp.status_code not in _BACKOFF_RETRY_STATUS:
|
|
140
147
|
return resp
|
|
141
148
|
|
|
142
149
|
if attempt == max_attempts - 1:
|
|
143
150
|
logger.warning(
|
|
144
|
-
"Zhipu
|
|
151
|
+
"Zhipu %d overload exhausted after %d attempts",
|
|
152
|
+
resp.status_code,
|
|
145
153
|
max_attempts,
|
|
146
154
|
)
|
|
147
155
|
return resp
|
|
@@ -150,7 +158,8 @@ class ZhipuVendor(NativeAnthropicVendor):
|
|
|
150
158
|
resp.response_headers, attempt
|
|
151
159
|
)
|
|
152
160
|
logger.info(
|
|
153
|
-
"Zhipu
|
|
161
|
+
"Zhipu %d overload, retry %d/%d in %.1fms",
|
|
162
|
+
resp.status_code,
|
|
154
163
|
attempt + 1,
|
|
155
164
|
max_attempts - 1,
|
|
156
165
|
delay,
|
|
@@ -159,16 +168,16 @@ class ZhipuVendor(NativeAnthropicVendor):
|
|
|
159
168
|
|
|
160
169
|
return resp # pragma: no cover
|
|
161
170
|
|
|
162
|
-
# ── 流式:429 重试
|
|
171
|
+
# ── 流式:429/529 重试 ──────────────────────────────────
|
|
163
172
|
|
|
164
173
|
async def send_message_stream(
|
|
165
174
|
self,
|
|
166
175
|
request_body: dict[str, Any],
|
|
167
176
|
headers: dict[str, str],
|
|
168
177
|
) -> AsyncIterator[bytes]:
|
|
169
|
-
"""流式请求,429 时自动重试.
|
|
178
|
+
"""流式请求,429/529 时自动重试.
|
|
170
179
|
|
|
171
|
-
安全性:429 在 BaseVendor.send_message_stream 中于
|
|
180
|
+
安全性:429/529 在 BaseVendor.send_message_stream 中于
|
|
172
181
|
status code 检查阶段即 raise(在任何 chunk yield 之前),
|
|
173
182
|
因此重试不会导致已发出数据不一致。
|
|
174
183
|
"""
|
|
@@ -176,25 +185,33 @@ class ZhipuVendor(NativeAnthropicVendor):
|
|
|
176
185
|
|
|
177
186
|
for attempt in range(max_attempts):
|
|
178
187
|
try:
|
|
179
|
-
# 429 在 status code 检查阶段即 raise(在任何 chunk 之前),
|
|
188
|
+
# 429/529 在 status code 检查阶段即 raise(在任何 chunk 之前),
|
|
180
189
|
# 因此 __anext__ 安全:要么拿到首个 chunk,要么抛异常。
|
|
181
190
|
ait = super().send_message_stream(request_body, headers)
|
|
182
191
|
head = await ait.__anext__()
|
|
183
192
|
except StopAsyncIteration:
|
|
184
193
|
return
|
|
185
194
|
except httpx.HTTPStatusError as exc:
|
|
186
|
-
if
|
|
195
|
+
if (
|
|
196
|
+
exc.response is None
|
|
197
|
+
or exc.response.status_code not in _BACKOFF_RETRY_STATUS
|
|
198
|
+
):
|
|
187
199
|
raise
|
|
200
|
+
status_code = exc.response.status_code
|
|
188
201
|
if attempt == max_attempts - 1:
|
|
189
202
|
logger.warning(
|
|
190
|
-
"Zhipu
|
|
203
|
+
"Zhipu %d stream overload exhausted after %d attempts",
|
|
204
|
+
status_code,
|
|
191
205
|
max_attempts,
|
|
192
206
|
)
|
|
193
207
|
raise
|
|
194
208
|
|
|
195
|
-
delay = self.
|
|
209
|
+
delay = self._compute_retry_delay_from_headers(
|
|
210
|
+
exc.response.headers, attempt
|
|
211
|
+
)
|
|
196
212
|
logger.info(
|
|
197
|
-
"Zhipu
|
|
213
|
+
"Zhipu %d stream overload, retry %d/%d in %.1fms",
|
|
214
|
+
status_code,
|
|
198
215
|
attempt + 1,
|
|
199
216
|
max_attempts - 1,
|
|
200
217
|
delay,
|
|
@@ -215,24 +232,15 @@ class ZhipuVendor(NativeAnthropicVendor):
|
|
|
215
232
|
headers: dict[str, str] | None,
|
|
216
233
|
attempt: int,
|
|
217
234
|
) -> float:
|
|
218
|
-
"""计算重试延迟(毫秒),优先使用 server retry-after.
|
|
219
|
-
rl_info = parse_rate_limit_headers(headers, 429, None)
|
|
220
|
-
server_delay_s = compute_effective_retry_seconds(rl_info)
|
|
221
|
-
if server_delay_s is not None:
|
|
222
|
-
return min(server_delay_s * 1000, self._rl_retry.max_delay_ms)
|
|
223
|
-
return calculate_delay(attempt, self._rl_retry)
|
|
235
|
+
"""计算重试延迟(毫秒),优先使用 server retry-after.
|
|
224
236
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"""
|
|
231
|
-
rl_info = parse_rate_limit_headers(
|
|
232
|
-
response.headers,
|
|
233
|
-
response.status_code,
|
|
234
|
-
response.text[:500] if response.text else None,
|
|
235
|
-
)
|
|
237
|
+
非流式与流式 429/529 重试共用此方法(单一延迟逻辑)。
|
|
238
|
+
以"限流退避"语义(429)解析 header:``parse_rate_limit_headers``
|
|
239
|
+
仅对 429/403 解析 retry-after,故此处固定传 429,
|
|
240
|
+
使 529 也能尊重 server retry-after,与 429 行为一致。
|
|
241
|
+
无 server 信号时回退到指数退避 + Full Jitter。
|
|
242
|
+
"""
|
|
243
|
+
rl_info = parse_rate_limit_headers(headers, 429, None)
|
|
236
244
|
server_delay_s = compute_effective_retry_seconds(rl_info)
|
|
237
245
|
if server_delay_s is not None:
|
|
238
246
|
return min(server_delay_s * 1000, self._rl_retry.max_delay_ms)
|
|
@@ -356,6 +356,20 @@ def _make_429_response(
|
|
|
356
356
|
)
|
|
357
357
|
|
|
358
358
|
|
|
359
|
+
def _make_529_response(
|
|
360
|
+
headers: dict[str, str] | None = None,
|
|
361
|
+
) -> httpx.Response:
|
|
362
|
+
"""构造 529 HTTP 响应(Overloaded / 并发过载)."""
|
|
363
|
+
return httpx.Response(
|
|
364
|
+
status_code=529,
|
|
365
|
+
content=b'{"error":{"type":"overloaded_error","message":"Overloaded"}}',
|
|
366
|
+
headers=headers or {},
|
|
367
|
+
request=httpx.Request(
|
|
368
|
+
"POST", "https://open.bigmodel.cn/api/anthropic/v1/messages"
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
359
373
|
def _make_200_response() -> httpx.Response:
|
|
360
374
|
"""构造 200 HTTP 响应."""
|
|
361
375
|
body = json.dumps(
|
|
@@ -666,3 +680,170 @@ class TestRateLimitRetry:
|
|
|
666
680
|
{},
|
|
667
681
|
)
|
|
668
682
|
assert resp.status_code == 401
|
|
683
|
+
|
|
684
|
+
# ── 529 Overloaded(并发过载)重试,行为与 429 一致 ──────
|
|
685
|
+
|
|
686
|
+
@pytest.mark.asyncio
|
|
687
|
+
async def test_nonstream_529_retries_and_succeeds(self):
|
|
688
|
+
"""非流式 529 两次后 200,重试成功."""
|
|
689
|
+
vendor = _make_zhipu_vendor()
|
|
690
|
+
call_count = 0
|
|
691
|
+
|
|
692
|
+
async def mock_post(*args, **kwargs):
|
|
693
|
+
nonlocal call_count
|
|
694
|
+
call_count += 1
|
|
695
|
+
if call_count <= 2:
|
|
696
|
+
return _make_529_response()
|
|
697
|
+
return _make_200_response()
|
|
698
|
+
|
|
699
|
+
with (
|
|
700
|
+
patch.object(vendor, "_get_client") as mock_client,
|
|
701
|
+
patch("asyncio.sleep", new_callable=AsyncMock),
|
|
702
|
+
):
|
|
703
|
+
client = AsyncMock()
|
|
704
|
+
client.post = mock_post
|
|
705
|
+
mock_client.return_value = client
|
|
706
|
+
|
|
707
|
+
resp = await vendor.send_message(
|
|
708
|
+
{"model": "claude-sonnet-4-20250514", "messages": []},
|
|
709
|
+
{},
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
assert resp.status_code == 200
|
|
713
|
+
assert call_count == 3
|
|
714
|
+
|
|
715
|
+
@pytest.mark.asyncio
|
|
716
|
+
async def test_nonstream_529_exhausted_retries(self):
|
|
717
|
+
"""非流式连续 5 次 529,耗尽重试后返回 529."""
|
|
718
|
+
vendor = _make_zhipu_vendor()
|
|
719
|
+
call_count = 0
|
|
720
|
+
|
|
721
|
+
async def mock_post(*args, **kwargs):
|
|
722
|
+
nonlocal call_count
|
|
723
|
+
call_count += 1
|
|
724
|
+
return _make_529_response()
|
|
725
|
+
|
|
726
|
+
with (
|
|
727
|
+
patch.object(vendor, "_get_client") as mock_client,
|
|
728
|
+
patch("asyncio.sleep", new_callable=AsyncMock),
|
|
729
|
+
):
|
|
730
|
+
client = AsyncMock()
|
|
731
|
+
client.post = mock_post
|
|
732
|
+
mock_client.return_value = client
|
|
733
|
+
|
|
734
|
+
resp = await vendor.send_message(
|
|
735
|
+
{"model": "claude-sonnet-4-20250514", "messages": []},
|
|
736
|
+
{},
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
assert resp.status_code == 529
|
|
740
|
+
assert call_count == 5
|
|
741
|
+
|
|
742
|
+
@pytest.mark.asyncio
|
|
743
|
+
async def test_stream_529_retries_and_succeeds(self):
|
|
744
|
+
"""流式 529 两次后成功."""
|
|
745
|
+
call_count = 0
|
|
746
|
+
|
|
747
|
+
async def fake_stream(self, body, headers):
|
|
748
|
+
nonlocal call_count
|
|
749
|
+
call_count += 1
|
|
750
|
+
if call_count <= 2:
|
|
751
|
+
resp = _make_529_response()
|
|
752
|
+
raise httpx.HTTPStatusError(
|
|
753
|
+
"529",
|
|
754
|
+
request=resp.request,
|
|
755
|
+
response=resp,
|
|
756
|
+
)
|
|
757
|
+
yield b'data: {"type":"content_block_start"}\n\n'
|
|
758
|
+
yield b'data: {"type":"content_block_delta"}\n\n'
|
|
759
|
+
|
|
760
|
+
vendor = _make_zhipu_vendor()
|
|
761
|
+
chunks = []
|
|
762
|
+
with (
|
|
763
|
+
patch.object(NativeAnthropicVendor, "send_message_stream", fake_stream),
|
|
764
|
+
patch("asyncio.sleep", new_callable=AsyncMock),
|
|
765
|
+
):
|
|
766
|
+
async for chunk in vendor.send_message_stream(
|
|
767
|
+
{"model": "claude-sonnet-4-20250514", "messages": []},
|
|
768
|
+
{},
|
|
769
|
+
):
|
|
770
|
+
chunks.append(chunk)
|
|
771
|
+
|
|
772
|
+
assert len(chunks) == 2
|
|
773
|
+
assert call_count == 3
|
|
774
|
+
|
|
775
|
+
@pytest.mark.asyncio
|
|
776
|
+
async def test_stream_529_exhausted_retries_raises(self):
|
|
777
|
+
"""流式连续 529,耗尽重试后 raise."""
|
|
778
|
+
call_count = 0
|
|
779
|
+
|
|
780
|
+
async def fake_stream(self, body, headers):
|
|
781
|
+
nonlocal call_count
|
|
782
|
+
call_count += 1
|
|
783
|
+
resp = _make_529_response()
|
|
784
|
+
raise httpx.HTTPStatusError(
|
|
785
|
+
"529",
|
|
786
|
+
request=resp.request,
|
|
787
|
+
response=resp,
|
|
788
|
+
)
|
|
789
|
+
yield # 使函数成为 async generator(不可达,仅影响类型)
|
|
790
|
+
|
|
791
|
+
vendor = _make_zhipu_vendor()
|
|
792
|
+
with (
|
|
793
|
+
patch.object(NativeAnthropicVendor, "send_message_stream", fake_stream),
|
|
794
|
+
patch("asyncio.sleep", new_callable=AsyncMock),
|
|
795
|
+
pytest.raises(httpx.HTTPStatusError) as exc_info,
|
|
796
|
+
):
|
|
797
|
+
async for _ in vendor.send_message_stream(
|
|
798
|
+
{"model": "claude-sonnet-4-20250514", "messages": []},
|
|
799
|
+
{},
|
|
800
|
+
):
|
|
801
|
+
pass
|
|
802
|
+
|
|
803
|
+
assert exc_info.value.response.status_code == 529
|
|
804
|
+
assert call_count == 5
|
|
805
|
+
|
|
806
|
+
@pytest.mark.asyncio
|
|
807
|
+
async def test_stream_529_respects_retry_after(self):
|
|
808
|
+
"""流式 529 响应含 retry-after 时使用 server 建议延迟.
|
|
809
|
+
|
|
810
|
+
回归保障:修复前流式延迟计算把真实状态码 529 传给
|
|
811
|
+
parse_rate_limit_headers(仅对 429/403 解析),导致 529 忽略
|
|
812
|
+
retry-after 而回退指数退避(首次最多 1s)。修复后固定按 429
|
|
813
|
+
语义解析,529 与 429 一样尊重 server retry-after。
|
|
814
|
+
"""
|
|
815
|
+
call_count = 0
|
|
816
|
+
sleep_delays = []
|
|
817
|
+
|
|
818
|
+
async def fake_stream(self, body, headers):
|
|
819
|
+
nonlocal call_count
|
|
820
|
+
call_count += 1
|
|
821
|
+
if call_count == 1:
|
|
822
|
+
resp = _make_529_response(headers={"retry-after": "2"})
|
|
823
|
+
raise httpx.HTTPStatusError(
|
|
824
|
+
"529",
|
|
825
|
+
request=resp.request,
|
|
826
|
+
response=resp,
|
|
827
|
+
)
|
|
828
|
+
yield b'data: {"type":"content_block_start"}\n\n'
|
|
829
|
+
|
|
830
|
+
async def mock_sleep(delay):
|
|
831
|
+
sleep_delays.append(delay)
|
|
832
|
+
|
|
833
|
+
vendor = _make_zhipu_vendor()
|
|
834
|
+
chunks = []
|
|
835
|
+
with (
|
|
836
|
+
patch.object(NativeAnthropicVendor, "send_message_stream", fake_stream),
|
|
837
|
+
patch("asyncio.sleep", side_effect=mock_sleep),
|
|
838
|
+
):
|
|
839
|
+
async for chunk in vendor.send_message_stream(
|
|
840
|
+
{"model": "claude-sonnet-4-20250514", "messages": []},
|
|
841
|
+
{},
|
|
842
|
+
):
|
|
843
|
+
chunks.append(chunk)
|
|
844
|
+
|
|
845
|
+
assert len(chunks) == 1
|
|
846
|
+
assert call_count == 2
|
|
847
|
+
assert len(sleep_delays) == 1
|
|
848
|
+
# retry-after=2 → 2 * 1.1 = 2.2s(>1s 指数退避首跳,证明用了 server 信号)
|
|
849
|
+
assert 2.0 <= sleep_delays[0] <= 2.2
|
|
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
|