coding-proxy 0.3.1a5__tar.gz → 0.3.1a7__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.1a5 → coding_proxy-0.3.1a7}/AGENTS.md +1 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/PKG-INFO +1 -1
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/issue.md +47 -13
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/pyproject.toml +1 -1
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/vendor_channels.py +122 -57
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/vendor.py +2 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/error_classifier.py +13 -5
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_error_classifier.py +72 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_vendor.py +12 -3
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_router_executor.py +5 -10
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_types.py +1 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_vendor_channels.py +83 -110
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_vendors.py +36 -2
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/uv.lock +1 -1
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.gitignore +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/CHANGELOG.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/LICENSE +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/README.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/assets/dashboard-v0.2.4.png +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_zhipu.py +0 -0
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
1. **Python**: 严禁使用 pip/poetry,**必须**统一使用 `uv` 进行包管理与脚本执行(如 `uv run`);
|
|
53
53
|
2. **JavaScript/TypeScript**: 严禁使用 npm/yarn,**必须**统一使用 `pnpm` 进行包管理与脚本执行。
|
|
54
54
|
- **Database Management**: 谨慎操作,数据迁移、测试等操作严禁将现有数据删除,谨慎操作数据迁移的回滚,防止数据被清理。
|
|
55
|
+
- **In-depth and close to the facts**:系统且全面地进行问题的分析,深入贴近事实,如有疑问,需先发问,不要乱做决定。
|
|
55
56
|
|
|
56
57
|
## Documentation Standards (文档规范)
|
|
57
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1a7
|
|
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
|
|
@@ -152,30 +152,64 @@ zhipu GLM-5 在处理含 `tool_result` 块的会话时持续返回 500 错误,
|
|
|
152
152
|
|
|
153
153
|
```
|
|
154
154
|
WARNING zhipu stream error: status=500 body='...message":"\'ClaudeContentBlockToolResult\' object has no attribute \'id\'"}'
|
|
155
|
-
WARNING Tier zhipu zhipu tool_result format error (500), treating as format incompatibility without circuit breaker penalty
|
|
156
|
-
INFO Failover: zhipu → copilot (reason: HTTP 500)
|
|
157
155
|
```
|
|
158
156
|
|
|
159
157
|
**表因**
|
|
160
158
|
|
|
161
|
-
zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id`
|
|
159
|
+
zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id` 字段。
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
**根因**(2026-04-29 第二次复盘更新)
|
|
164
162
|
|
|
165
|
-
|
|
163
|
+
**第一次诊断**(已推翻):认为 `_inject_tool_result_id_for_zhipu` 注入 `id` 可绕过。实证:注入 114 个块后 500 依旧。
|
|
166
164
|
|
|
167
|
-
|
|
165
|
+
**第二次诊断**(已推翻):认为 `enforce_anthropic_tool_pairing` 搬迁 tool_result 到 user 消息是触发条件。实证:移除 tool pairing 后 500 依旧(日志显示 `copilot → zhipu: stripped_19_thinking_blocks, removed_thinking_param`,无 `misplaced_tool_result_relocated`)。
|
|
166
|
+
|
|
167
|
+
**实际根因**:zhipu 后端的 `ClaudeContentBlockToolResult` Python 类**没有 `id` 属性**,但 zhipu 代码在处理**所有** `tool_result` 块时都访问 `obj.id`,无论块位于 assistant 还是 user 消息。三层因果链:
|
|
168
|
+
|
|
169
|
+
1. **zhipu 后端 Bug**(不可修复 — 上游代码):`ClaudeContentBlockToolResult` 类缺少 `id` 属性,zhipu 代码访问时触发 `AttributeError` → 500。
|
|
170
|
+
2. **JSON 注入无效**(已实证):`_inject_tool_result_id_for_zhipu` 往 JSON dict 注入 `id = tool_use_id`,但 zhipu 反序列化框架不读取此字段,Python 对象仍无 `id` 属性。
|
|
171
|
+
3. **无预防机制**(proxy 层可修复):tier 门控系统不检查请求是否含 `tool_result` 块 → 每次请求先发 zhipu → 必然 500 → failover → 额外 ~2 秒延迟。
|
|
172
|
+
|
|
173
|
+
**实证依据**:
|
|
174
|
+
- 有注入(114 个块)→ 500;无注入 → 500。结论:注入无效。
|
|
175
|
+
- 有 tool pairing → 500;无 tool pairing → 500。结论:tool pairing 不是触发条件。
|
|
176
|
+
- 首次请求(无 tool_result 块)→ zhipu 正常。结论:500 由 tool_result 块本身触发。
|
|
177
|
+
|
|
178
|
+
**处理方式**(2026-04-29 第二次更新)
|
|
179
|
+
|
|
180
|
+
从所有 zhipu 目标转换通道中移除有害/无效步骤(`enforce_anthropic_tool_pairing`、`_inject_tool_result_id_for_zhipu`、`_strip_cache_control`),并在 `ZhipuVendor.supports_request` 中增加 `has_tool_results` 门控:当请求包含 `tool_result` 块时主动拒绝 zhipu tier,避免「尝试 → 500 → failover」的无效延迟。
|
|
181
|
+
|
|
182
|
+
| 变更项 | 说明 |
|
|
183
|
+
|--------|------|
|
|
184
|
+
| `RequestCapabilities.has_tool_results` | 新增字段,检测请求中是否含 `tool_result` 块 |
|
|
185
|
+
| `CapabilityLossReason.TOOL_RESULTS` | 新增枚举值,标记 tool_result 兼容性问题 |
|
|
186
|
+
| `ZhipuVendor.supports_request` | 覆写方法,`has_tool_results=True` 时拒绝请求 |
|
|
187
|
+
| `build_request_capabilities` | 扩展 tool_result 块检测逻辑 |
|
|
188
|
+
|
|
189
|
+
保留的 zhipu 目标转换通道精简步骤:
|
|
190
|
+
|
|
191
|
+
| 保留项 | 原因 |
|
|
192
|
+
|--------|------|
|
|
193
|
+
| `strip_thinking_blocks` | copilot/anthropic 的 thinking 签名 zhipu 无法验证 |
|
|
194
|
+
| 移除 `thinking`/`extended_thinking` 顶层参数 | zhipu 不支持 |
|
|
195
|
+
| `_remove_vendor_blocks(server_tool_use_delta)` | zhipu 自身流式残块 |
|
|
196
|
+
| `_remove_vendor_blocks(server_tool_use)` | Anthropic beta 块,zhipu 不支持 |
|
|
197
|
+
|
|
198
|
+
**涉及变更的转换通道**:
|
|
199
|
+
- `prepare_copilot_to_zhipu` — 移除 cache_control / tool pairing / id 注入
|
|
200
|
+
- `prepare_anthropic_to_zhipu` — 移除 cache_control / tool pairing / id 注入
|
|
201
|
+
- `prepare_zhipu_self_cleanup` — 移除 tool pairing / id 注入
|
|
168
202
|
|
|
169
|
-
|
|
170
|
-
- 在三个 targeting zhipu 的转换通道末尾统一调用此辅助函数
|
|
171
|
-
- 保留 executor 中已有的 500 错误检测作为纵深防御
|
|
203
|
+
**注意**: `prepare_zhipu_to_anthropic` 和 `prepare_zhipu_to_copilot` 不受影响(目标是 anthropic/copilot,不是 zhipu),仍保留 `enforce_anthropic_tool_pairing`。
|
|
172
204
|
|
|
173
205
|
**后续防范**
|
|
174
206
|
|
|
175
|
-
-
|
|
176
|
-
-
|
|
207
|
+
- **转换通道的「最小干预」原则**:跨供应商转换应仅清理目标供应商**确认不支持**的特性。未经验证的「预防性清理」(如剥离 cache_control)可能误伤供应商原生支持的功能,甚至引入新的故障。
|
|
208
|
+
- **workaround 须验证有效**:`_inject_tool_result_id_for_zhipu` 虽有注释说明目的,但未经验证其有效性即合入。后续 workaround 须附带验证证据(如 curl 复现、上游确认)。
|
|
209
|
+
- **zhipu 后端 bug 跟踪**:`ClaudeContentBlockToolResult` 类缺少 `id` 属性是 zhipu 上游 bug。若 zhipu 修复此 bug,可考虑恢复 tool pairing 以获得更严格的消息结构校验。
|
|
177
210
|
|
|
178
211
|
**同类问题影响与处理注意事项**
|
|
179
212
|
|
|
180
|
-
- `
|
|
181
|
-
-
|
|
213
|
+
- `NativeAnthropicVendor` 子类的自清理通道应**精确剪裁**:仅修复 vendor 自身拒绝的产物,不做跨供应商的全量清理。
|
|
214
|
+
- 当 zhipu 后端出现新的 400 拒绝(如 inline tool_result 再次被拒),应优先调查是 zhipu 后端变更还是请求格式问题,而非立即加回 tool pairing(可能重新触发 500)。
|
|
215
|
+
- `_inject_tool_result_id_for_zhipu` 函数暂时保留在代码中(未删除),标记为 deprecated,待确认不需要后清理。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1a7"
|
|
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"
|
|
@@ -352,6 +352,92 @@ def _inject_tool_result_id_for_zhipu(body: dict[str, Any]) -> int:
|
|
|
352
352
|
return injected
|
|
353
353
|
|
|
354
354
|
|
|
355
|
+
def _extract_text_from_content(content: Any) -> str:
|
|
356
|
+
"""从 tool_result 的 content 字段提取可读文本."""
|
|
357
|
+
if isinstance(content, str):
|
|
358
|
+
return content
|
|
359
|
+
if isinstance(content, list):
|
|
360
|
+
parts: list[str] = []
|
|
361
|
+
for item in content:
|
|
362
|
+
if isinstance(item, str):
|
|
363
|
+
parts.append(item)
|
|
364
|
+
elif isinstance(item, dict) and item.get("type") == "text":
|
|
365
|
+
parts.append(item.get("text", ""))
|
|
366
|
+
return " ".join(parts)
|
|
367
|
+
return ""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _flatten_tool_blocks(body: dict[str, Any]) -> int:
|
|
371
|
+
"""将 messages 中的 tool_use 和 tool_result 块转为 text 块.
|
|
372
|
+
|
|
373
|
+
zhipu GLM-5 后端的 ``ClaudeContentBlockToolResult`` 类缺少 ``id`` 属性,
|
|
374
|
+
导致处理 tool_result 块时触发 ``AttributeError`` → HTTP 500。
|
|
375
|
+
此函数将所有 tool_use / tool_result 块转为纯文本表示,
|
|
376
|
+
让 zhipu 以普通文本对话处理,彻底规避反序列化缺陷。
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
被转换的 tool_use + tool_result 块总数。
|
|
380
|
+
"""
|
|
381
|
+
import json as _json
|
|
382
|
+
|
|
383
|
+
converted = 0
|
|
384
|
+
for message in body.get("messages", []):
|
|
385
|
+
if not isinstance(message, dict):
|
|
386
|
+
continue
|
|
387
|
+
content = message.get("content")
|
|
388
|
+
if not isinstance(content, list):
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
new_blocks: list[dict[str, Any]] = []
|
|
392
|
+
for block in content:
|
|
393
|
+
if not isinstance(block, dict):
|
|
394
|
+
new_blocks.append(block)
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
block_type = block.get("type")
|
|
398
|
+
|
|
399
|
+
if block_type == "tool_use":
|
|
400
|
+
name = block.get("name", "unknown")
|
|
401
|
+
input_data = block.get("input", {})
|
|
402
|
+
try:
|
|
403
|
+
args_text = _json.dumps(input_data, ensure_ascii=False)
|
|
404
|
+
except (TypeError, ValueError):
|
|
405
|
+
args_text = str(input_data)
|
|
406
|
+
# 截断过长参数
|
|
407
|
+
if len(args_text) > 2000:
|
|
408
|
+
args_text = args_text[:1997] + "..."
|
|
409
|
+
new_blocks.append(
|
|
410
|
+
{"type": "text", "text": f"[Tool Call: {name}({args_text})]"}
|
|
411
|
+
)
|
|
412
|
+
converted += 1
|
|
413
|
+
|
|
414
|
+
elif block_type == "tool_result":
|
|
415
|
+
tool_use_id = block.get("tool_use_id", "?")
|
|
416
|
+
is_error = block.get("is_error", False)
|
|
417
|
+
result_text = _extract_text_from_content(block.get("content"))
|
|
418
|
+
if len(result_text) > 2000:
|
|
419
|
+
result_text = result_text[:1997] + "..."
|
|
420
|
+
prefix = "[ERROR] " if is_error else ""
|
|
421
|
+
new_blocks.append(
|
|
422
|
+
{
|
|
423
|
+
"type": "text",
|
|
424
|
+
"text": f"{prefix}[Tool Result for {tool_use_id}: {result_text}]",
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
converted += 1
|
|
428
|
+
|
|
429
|
+
else:
|
|
430
|
+
new_blocks.append(block)
|
|
431
|
+
|
|
432
|
+
# 如果 content 为空则插入占位
|
|
433
|
+
if not new_blocks:
|
|
434
|
+
new_blocks = [{"type": "text", "text": "..."}]
|
|
435
|
+
|
|
436
|
+
message["content"] = new_blocks
|
|
437
|
+
|
|
438
|
+
return converted
|
|
439
|
+
|
|
440
|
+
|
|
355
441
|
def _strip_cache_control(body: dict[str, Any]) -> int:
|
|
356
442
|
"""从 system/messages/tools 中移除 cache_control 字段(就地).
|
|
357
443
|
|
|
@@ -572,13 +658,18 @@ def infer_source_vendor_from_body(body: dict[str, Any]) -> str | None:
|
|
|
572
658
|
def prepare_copilot_to_zhipu(
|
|
573
659
|
body: dict[str, Any],
|
|
574
660
|
) -> tuple[dict[str, Any], list[str]]:
|
|
575
|
-
"""copilot → zhipu 转换:
|
|
661
|
+
"""copilot → zhipu 转换: 仅清理 copilot 产物中 zhipu 确认不支持的部分.
|
|
662
|
+
|
|
663
|
+
GLM-5 的 Anthropic 兼容端点:
|
|
664
|
+
- ✗ thinking / redacted_thinking 块 (signature 由非 Anthropic 签发)
|
|
665
|
+
- ✓ cache_control 字段 (cache_read 已在生产实证)
|
|
666
|
+
- ✓ tool_result 在 assistant 消息中内联 (zhipu 自身偶发产出,可自行消化)
|
|
667
|
+
- ✗ 顶层 thinking / extended_thinking 参数
|
|
576
668
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
- 顶层 thinking / extended_thinking 参数
|
|
669
|
+
注意: 不再执行 enforce_anthropic_tool_pairing 和 _inject_tool_result_id_for_zhipu。
|
|
670
|
+
实证表明 tool_result 重定位会触发 zhipu 后端 ``'ClaudeContentBlockToolResult'
|
|
671
|
+
object has no attribute 'id'`` 500 错误;id 注入对 zhipu 的 Python 类
|
|
672
|
+
(不读取 JSON 中的 id 字段) 亦无效。详见 docs/issue.md。
|
|
582
673
|
|
|
583
674
|
Returns:
|
|
584
675
|
(prepared_body, adaptations) — adaptations 为应用的变换描述列表。
|
|
@@ -591,26 +682,16 @@ def prepare_copilot_to_zhipu(
|
|
|
591
682
|
if stripped:
|
|
592
683
|
adaptations.append(f"stripped_{stripped}_thinking_blocks")
|
|
593
684
|
|
|
594
|
-
# Step 2:
|
|
595
|
-
removed_cc = _strip_cache_control(prepared)
|
|
596
|
-
if removed_cc:
|
|
597
|
-
adaptations.append(f"removed_{removed_cc}_cache_control_fields")
|
|
598
|
-
|
|
599
|
-
# Step 3: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
|
|
685
|
+
# Step 2: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
|
|
600
686
|
for param in ("thinking", "extended_thinking"):
|
|
601
687
|
if param in prepared:
|
|
602
688
|
del prepared[param]
|
|
603
689
|
adaptations.append(f"removed_{param}_param")
|
|
604
690
|
|
|
605
|
-
# Step
|
|
606
|
-
|
|
607
|
-
if
|
|
608
|
-
adaptations.
|
|
609
|
-
|
|
610
|
-
# Step 5: 为 tool_result 块注入 id 字段(zhipu 后端 bug workaround)
|
|
611
|
-
injected = _inject_tool_result_id_for_zhipu(prepared)
|
|
612
|
-
if injected:
|
|
613
|
-
adaptations.append(f"injected_{injected}_tool_result_id_fields")
|
|
691
|
+
# Step 3: 展平 tool_use/tool_result 为 text 块
|
|
692
|
+
flattened = _flatten_tool_blocks(prepared)
|
|
693
|
+
if flattened:
|
|
694
|
+
adaptations.append(f"flattened_{flattened}_tool_blocks")
|
|
614
695
|
|
|
615
696
|
return prepared, adaptations
|
|
616
697
|
|
|
@@ -632,9 +713,11 @@ def prepare_anthropic_to_zhipu(
|
|
|
632
713
|
Anthropic API 可能产生的非兼容产物:
|
|
633
714
|
- ``server_tool_use`` blocks(web search / computer use 等 beta 功能)
|
|
634
715
|
- ``thinking`` / ``redacted_thinking`` blocks(含 Anthropic 签发的 signature)
|
|
635
|
-
- ``cache_control`` 字段
|
|
636
716
|
- 顶层 ``thinking`` / ``extended_thinking`` 参数
|
|
637
717
|
|
|
718
|
+
注意: 不再移除 cache_control (GLM-5 支持) ,不再执行 tool pairing 和
|
|
719
|
+
id 注入。原因同 prepare_copilot_to_zhipu 的 docstring。
|
|
720
|
+
|
|
638
721
|
Returns:
|
|
639
722
|
(prepared_body, adaptations) — adaptations 为应用的变换描述列表。
|
|
640
723
|
"""
|
|
@@ -651,26 +734,16 @@ def prepare_anthropic_to_zhipu(
|
|
|
651
734
|
if stripped:
|
|
652
735
|
adaptations.append(f"stripped_{stripped}_thinking_blocks")
|
|
653
736
|
|
|
654
|
-
# Step 3:
|
|
655
|
-
removed_cc = _strip_cache_control(prepared)
|
|
656
|
-
if removed_cc:
|
|
657
|
-
adaptations.append(f"removed_{removed_cc}_cache_control_fields")
|
|
658
|
-
|
|
659
|
-
# Step 4: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
|
|
737
|
+
# Step 3: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
|
|
660
738
|
for param in ("thinking", "extended_thinking"):
|
|
661
739
|
if param in prepared:
|
|
662
740
|
del prepared[param]
|
|
663
741
|
adaptations.append(f"removed_{param}_param")
|
|
664
742
|
|
|
665
|
-
# Step
|
|
666
|
-
|
|
667
|
-
if
|
|
668
|
-
adaptations.
|
|
669
|
-
|
|
670
|
-
# Step 6: 为 tool_result 块注入 id 字段(zhipu 后端 bug workaround)
|
|
671
|
-
injected = _inject_tool_result_id_for_zhipu(prepared)
|
|
672
|
-
if injected:
|
|
673
|
-
adaptations.append(f"injected_{injected}_tool_result_id_fields")
|
|
743
|
+
# Step 4: 展平 tool_use/tool_result 为 text 块
|
|
744
|
+
flattened = _flatten_tool_blocks(prepared)
|
|
745
|
+
if flattened:
|
|
746
|
+
adaptations.append(f"flattened_{flattened}_tool_blocks")
|
|
674
747
|
|
|
675
748
|
return prepared, adaptations
|
|
676
749
|
|
|
@@ -782,25 +855,22 @@ def prepare_zhipu_to_anthropic(
|
|
|
782
855
|
def prepare_zhipu_self_cleanup(
|
|
783
856
|
body: dict[str, Any],
|
|
784
857
|
) -> tuple[dict[str, Any], list[str]]:
|
|
785
|
-
"""zhipu → zhipu 自清理:
|
|
858
|
+
"""zhipu → zhipu 自清理: 仅剥离 zhipu 自身的流式残块.
|
|
786
859
|
|
|
787
|
-
GLM-5
|
|
788
|
-
|
|
789
|
-
产物原样回送下一轮请求时,zhipu 的 Anthropic 兼容端点会以 400 拒绝
|
|
790
|
-
(表现为 "400 + tool_results" 偶发,进而触发到 copilot 的降级)。
|
|
860
|
+
GLM-5 在流式响应中偶发暴露 ``server_tool_use_delta`` 私有块。当 Claude Code
|
|
861
|
+
将这些产物原样回送下一轮请求时,zhipu 的 Anthropic 兼容端点会拒绝。
|
|
791
862
|
|
|
792
|
-
|
|
863
|
+
本通道**保留**所有 zhipu 原生支持的特性:
|
|
793
864
|
|
|
794
865
|
- ✓ ``srvtoolu_*`` ID 与 ``server_tool_use`` 类型(zhipu 原生)
|
|
795
866
|
- ✓ thinking blocks 的 zhipu 自签 signature
|
|
796
867
|
- ✓ ``cache_control`` 字段(GLM Anthropic 端点支持,cache_read 已实证)
|
|
797
868
|
- ✓ 顶层 ``thinking`` / ``extended_thinking`` 参数
|
|
869
|
+
- ✓ tool_result 在 assistant 消息中内联(zhipu 自身偶发产出,可自行消化)
|
|
798
870
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
搬迁到紧随的 user 消息)
|
|
803
|
-
3. 为 ``tool_result`` 块注入 ``id`` 字段(zhipu 后端错误访问 ``.id`` 属性)
|
|
871
|
+
注意: 不再执行 enforce_anthropic_tool_pairing 和 _inject_tool_result_id_for_zhipu。
|
|
872
|
+
实证表明 tool_result 重定位会触发 zhipu 后端 500 错误。
|
|
873
|
+
详见 docs/issue.md。
|
|
804
874
|
|
|
805
875
|
Returns:
|
|
806
876
|
(prepared_body, adaptations) — adaptations 为应用的变换描述列表。
|
|
@@ -813,15 +883,10 @@ def prepare_zhipu_self_cleanup(
|
|
|
813
883
|
if removed_vendor_blocks:
|
|
814
884
|
adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
|
|
815
885
|
|
|
816
|
-
# Step 2:
|
|
817
|
-
|
|
818
|
-
if
|
|
819
|
-
adaptations.
|
|
820
|
-
|
|
821
|
-
# Step 3: 为 tool_result 块注入 id 字段(zhipu 后端 bug workaround)
|
|
822
|
-
injected = _inject_tool_result_id_for_zhipu(prepared)
|
|
823
|
-
if injected:
|
|
824
|
-
adaptations.append(f"injected_{injected}_tool_result_id_fields")
|
|
886
|
+
# Step 2: 展平 tool_use/tool_result 为 text 块
|
|
887
|
+
flattened = _flatten_tool_blocks(prepared)
|
|
888
|
+
if flattened:
|
|
889
|
+
adaptations.append(f"flattened_{flattened}_tool_blocks")
|
|
825
890
|
|
|
826
891
|
return prepared, adaptations
|
|
827
892
|
|
|
@@ -99,6 +99,7 @@ class CapabilityLossReason(Enum):
|
|
|
99
99
|
IMAGES = "images"
|
|
100
100
|
VENDOR_TOOLS = "vendor_tools"
|
|
101
101
|
METADATA = "metadata"
|
|
102
|
+
TOOL_RESULTS = "tool_results"
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
@dataclass(frozen=True)
|
|
@@ -109,6 +110,7 @@ class RequestCapabilities:
|
|
|
109
110
|
has_thinking: bool = False
|
|
110
111
|
has_images: bool = False
|
|
111
112
|
has_metadata: bool = False
|
|
113
|
+
has_tool_results: bool = False
|
|
112
114
|
|
|
113
115
|
|
|
114
116
|
@dataclass(frozen=True)
|
|
@@ -111,15 +111,22 @@ def is_semantic_rejection(
|
|
|
111
111
|
def build_request_capabilities(body: dict[str, Any]) -> RequestCapabilities:
|
|
112
112
|
"""从请求体提取能力画像."""
|
|
113
113
|
has_images = False
|
|
114
|
+
has_tool_results = False
|
|
114
115
|
for msg in body.get("messages", []):
|
|
115
116
|
content = msg.get("content")
|
|
116
117
|
if not isinstance(content, list):
|
|
117
118
|
continue
|
|
118
|
-
|
|
119
|
-
isinstance(block, dict)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
for block in content:
|
|
120
|
+
if not isinstance(block, dict):
|
|
121
|
+
continue
|
|
122
|
+
block_type = block.get("type")
|
|
123
|
+
if block_type == "image" and not has_images:
|
|
124
|
+
has_images = True
|
|
125
|
+
elif block_type == "tool_result" and not has_tool_results:
|
|
126
|
+
has_tool_results = True
|
|
127
|
+
if has_images and has_tool_results:
|
|
128
|
+
break
|
|
129
|
+
if has_images and has_tool_results:
|
|
123
130
|
break
|
|
124
131
|
|
|
125
132
|
return RequestCapabilities(
|
|
@@ -127,4 +134,5 @@ def build_request_capabilities(body: dict[str, Any]) -> RequestCapabilities:
|
|
|
127
134
|
has_thinking=bool(body.get("thinking") or body.get("extended_thinking")),
|
|
128
135
|
has_images=has_images,
|
|
129
136
|
has_metadata=bool(body.get("metadata")),
|
|
137
|
+
has_tool_results=has_tool_results,
|
|
130
138
|
)
|
|
@@ -362,3 +362,75 @@ def test_string_content_not_image():
|
|
|
362
362
|
def test_empty_messages():
|
|
363
363
|
caps = build_request_capabilities({"model": "m", "messages": []})
|
|
364
364
|
assert caps.has_images is False
|
|
365
|
+
assert caps.has_tool_results is False
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_tool_results_in_user_message():
|
|
369
|
+
caps = build_request_capabilities(
|
|
370
|
+
{
|
|
371
|
+
"model": "m",
|
|
372
|
+
"messages": [
|
|
373
|
+
{
|
|
374
|
+
"role": "user",
|
|
375
|
+
"content": [
|
|
376
|
+
{
|
|
377
|
+
"type": "tool_result",
|
|
378
|
+
"tool_use_id": "toolu_1",
|
|
379
|
+
"content": "ok",
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
assert caps.has_tool_results is True
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_tool_results_in_assistant_message():
|
|
390
|
+
caps = build_request_capabilities(
|
|
391
|
+
{
|
|
392
|
+
"model": "m",
|
|
393
|
+
"messages": [
|
|
394
|
+
{
|
|
395
|
+
"role": "assistant",
|
|
396
|
+
"content": [
|
|
397
|
+
{
|
|
398
|
+
"type": "tool_use",
|
|
399
|
+
"id": "toolu_1",
|
|
400
|
+
"name": "bash",
|
|
401
|
+
"input": {},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"type": "tool_result",
|
|
405
|
+
"tool_use_id": "toolu_1",
|
|
406
|
+
"content": "ok",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
}
|
|
410
|
+
],
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
assert caps.has_tool_results is True
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_no_tool_results():
|
|
417
|
+
caps = build_request_capabilities(
|
|
418
|
+
{
|
|
419
|
+
"model": "m",
|
|
420
|
+
"messages": [
|
|
421
|
+
{"role": "user", "content": "hello"},
|
|
422
|
+
{
|
|
423
|
+
"role": "assistant",
|
|
424
|
+
"content": [
|
|
425
|
+
{
|
|
426
|
+
"type": "tool_use",
|
|
427
|
+
"id": "toolu_1",
|
|
428
|
+
"name": "bash",
|
|
429
|
+
"input": {},
|
|
430
|
+
}
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
assert caps.has_tool_results is False
|
|
@@ -115,13 +115,20 @@ class TestCapabilityLossReason:
|
|
|
115
115
|
assert CapabilityLossReason.METADATA.value == "metadata"
|
|
116
116
|
|
|
117
117
|
def test_member_count(self):
|
|
118
|
-
"""
|
|
119
|
-
assert len(CapabilityLossReason) ==
|
|
118
|
+
"""枚举成员数量."""
|
|
119
|
+
assert len(CapabilityLossReason) == 6
|
|
120
120
|
|
|
121
121
|
def test_can_iterate(self):
|
|
122
122
|
"""可遍历所有成员."""
|
|
123
123
|
names = {m.name for m in CapabilityLossReason}
|
|
124
|
-
assert names == {
|
|
124
|
+
assert names == {
|
|
125
|
+
"TOOLS",
|
|
126
|
+
"THINKING",
|
|
127
|
+
"IMAGES",
|
|
128
|
+
"VENDOR_TOOLS",
|
|
129
|
+
"METADATA",
|
|
130
|
+
"TOOL_RESULTS",
|
|
131
|
+
}
|
|
125
132
|
|
|
126
133
|
def test_lookup_by_value(self):
|
|
127
134
|
"""可通过 value 反查成员."""
|
|
@@ -144,6 +151,7 @@ class TestRequestCapabilities:
|
|
|
144
151
|
assert caps.has_thinking is False
|
|
145
152
|
assert caps.has_images is False
|
|
146
153
|
assert caps.has_metadata is False
|
|
154
|
+
assert caps.has_tool_results is False
|
|
147
155
|
|
|
148
156
|
def test_custom_true_values(self):
|
|
149
157
|
"""自定义构造: 指定 True 的字段正确赋值."""
|
|
@@ -152,6 +160,7 @@ class TestRequestCapabilities:
|
|
|
152
160
|
assert caps.has_images is True
|
|
153
161
|
assert caps.has_thinking is False
|
|
154
162
|
assert caps.has_metadata is False
|
|
163
|
+
assert caps.has_tool_results is False
|
|
155
164
|
|
|
156
165
|
def test_frozen_immutable(self):
|
|
157
166
|
"""frozen dataclass: 赋值操作抛 AttributeError."""
|
|
@@ -2034,7 +2034,7 @@ class TestPrepareBodyForTierSelfTransition:
|
|
|
2034
2034
|
"""验证 zhipu → zhipu 自转换通道在 _prepare_body_for_tier 中的应用行为."""
|
|
2035
2035
|
|
|
2036
2036
|
def test_applies_zhipu_self_cleanup(self):
|
|
2037
|
-
"""source=zhipu, target=zhipu → 剥离 server_tool_use_delta
|
|
2037
|
+
"""source=zhipu, target=zhipu → 剥离 server_tool_use_delta 并展平 tool 块."""
|
|
2038
2038
|
tier = MagicMock()
|
|
2039
2039
|
tier.name = "zhipu"
|
|
2040
2040
|
|
|
@@ -2067,17 +2067,12 @@ class TestPrepareBodyForTierSelfTransition:
|
|
|
2067
2067
|
assert result is not body
|
|
2068
2068
|
assert len(body["messages"][0]["content"]) == 3
|
|
2069
2069
|
|
|
2070
|
-
# delta
|
|
2070
|
+
# delta 块被剥离
|
|
2071
2071
|
assistant_content = result["messages"][0]["content"]
|
|
2072
|
+
assert all(b.get("type") != "server_tool_use_delta" for b in assistant_content)
|
|
2073
|
+
# tool_use 和 tool_result 被展平为 text
|
|
2072
2074
|
assert all(
|
|
2073
|
-
b.get("type") not in ("
|
|
2074
|
-
for b in assistant_content
|
|
2075
|
-
)
|
|
2076
|
-
# tool_result 已搬到下一个 user 消息
|
|
2077
|
-
assert result["messages"][1]["role"] == "user"
|
|
2078
|
-
assert any(
|
|
2079
|
-
b.get("type") == "tool_result" and b.get("tool_use_id") == "srvtoolu_a"
|
|
2080
|
-
for b in result["messages"][1]["content"]
|
|
2075
|
+
b.get("type") not in ("tool_use", "tool_result") for b in assistant_content
|
|
2081
2076
|
)
|
|
2082
2077
|
|
|
2083
2078
|
def test_self_cleanup_preserves_srvtoolu_ids(self):
|