coding-proxy 0.3.1a6__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.1a6 → coding_proxy-0.3.1a7}/PKG-INFO +1 -1
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/issue.md +20 -14
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/pyproject.toml +1 -1
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/vendor_channels.py +101 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/vendor.py +2 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/error_classifier.py +13 -5
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_error_classifier.py +72 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_vendor.py +12 -3
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_router_executor.py +4 -12
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_types.py +1 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_vendor_channels.py +54 -60
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_vendors.py +36 -2
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/uv.lock +1 -1
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.gitignore +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/AGENTS.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/CHANGELOG.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/LICENSE +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/README.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/assets/dashboard-v0.2.4.png +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/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.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
|
|
@@ -158,29 +158,35 @@ WARNING zhipu stream error: status=500 body='...message":"\'ClaudeContentBlockTo
|
|
|
158
158
|
|
|
159
159
|
zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id` 字段。
|
|
160
160
|
|
|
161
|
-
**根因**(2026-04-29
|
|
161
|
+
**根因**(2026-04-29 第二次复盘更新)
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
**第一次诊断**(已推翻):认为 `_inject_tool_result_id_for_zhipu` 注入 `id` 可绕过。实证:注入 114 个块后 500 依旧。
|
|
164
164
|
|
|
165
|
-
|
|
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
166
|
|
|
167
|
-
|
|
168
|
-
2. **转换后**:所有 zhipu 目标通道执行 `enforce_anthropic_tool_pairing`,将 assistant 内联的 `tool_result` 搬迁到紧随的 user 消息。zhipu 后端对 user 消息中的 `tool_result` **执行 `.id` 属性访问**(代码路径不同),触发 `AttributeError` → 500。
|
|
169
|
-
3. **`_inject_tool_result_id_for_zhipu` 无效**:该函数往 JSON dict 注入 `"id": tool_use_id`,但 zhipu 后端的 `ClaudeContentBlockToolResult` Python 类不从 JSON 读取 `id` 字段(类定义中无此属性),注入的值在反序列化时被丢弃。
|
|
167
|
+
**实际根因**:zhipu 后端的 `ClaudeContentBlockToolResult` Python 类**没有 `id` 属性**,但 zhipu 代码在处理**所有** `tool_result` 块时都访问 `obj.id`,无论块位于 assistant 还是 user 消息。三层因果链:
|
|
170
168
|
|
|
171
|
-
|
|
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
172
|
|
|
173
|
-
|
|
173
|
+
**实证依据**:
|
|
174
|
+
- 有注入(114 个块)→ 500;无注入 → 500。结论:注入无效。
|
|
175
|
+
- 有 tool pairing → 500;无 tool pairing → 500。结论:tool pairing 不是触发条件。
|
|
176
|
+
- 首次请求(无 tool_result 块)→ zhipu 正常。结论:500 由 tool_result 块本身触发。
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
**处理方式**(2026-04-29 第二次更新)
|
|
176
179
|
|
|
177
|
-
|
|
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
|
+
| 变更项 | 说明 |
|
|
178
183
|
|--------|------|
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
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 块检测逻辑 |
|
|
182
188
|
|
|
183
|
-
|
|
189
|
+
保留的 zhipu 目标转换通道精简步骤:
|
|
184
190
|
|
|
185
191
|
| 保留项 | 原因 |
|
|
186
192
|
|--------|------|
|
|
@@ -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
|
|
|
@@ -602,6 +688,11 @@ def prepare_copilot_to_zhipu(
|
|
|
602
688
|
del prepared[param]
|
|
603
689
|
adaptations.append(f"removed_{param}_param")
|
|
604
690
|
|
|
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")
|
|
695
|
+
|
|
605
696
|
return prepared, adaptations
|
|
606
697
|
|
|
607
698
|
|
|
@@ -649,6 +740,11 @@ def prepare_anthropic_to_zhipu(
|
|
|
649
740
|
del prepared[param]
|
|
650
741
|
adaptations.append(f"removed_{param}_param")
|
|
651
742
|
|
|
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")
|
|
747
|
+
|
|
652
748
|
return prepared, adaptations
|
|
653
749
|
|
|
654
750
|
|
|
@@ -787,6 +883,11 @@ def prepare_zhipu_self_cleanup(
|
|
|
787
883
|
if removed_vendor_blocks:
|
|
788
884
|
adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
|
|
789
885
|
|
|
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")
|
|
890
|
+
|
|
790
891
|
return prepared, adaptations
|
|
791
892
|
|
|
792
893
|
|
|
@@ -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,12 +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 →
|
|
2038
|
-
|
|
2039
|
-
不再做 tool pairing(搬迁 tool_result 会触发 zhipu 500),
|
|
2040
|
-
也不做 id 注入(zhipu 类不读取 JSON 中的 id)。
|
|
2041
|
-
inline tool_result 保留在 assistant 消息中,zhipu 可自行消化。
|
|
2042
|
-
"""
|
|
2037
|
+
"""source=zhipu, target=zhipu → 剥离 server_tool_use_delta 并展平 tool 块."""
|
|
2043
2038
|
tier = MagicMock()
|
|
2044
2039
|
tier.name = "zhipu"
|
|
2045
2040
|
|
|
@@ -2075,13 +2070,10 @@ class TestPrepareBodyForTierSelfTransition:
|
|
|
2075
2070
|
# delta 块被剥离
|
|
2076
2071
|
assistant_content = result["messages"][0]["content"]
|
|
2077
2072
|
assert all(b.get("type") != "server_tool_use_delta" for b in assistant_content)
|
|
2078
|
-
#
|
|
2079
|
-
assert
|
|
2080
|
-
b.get("type")
|
|
2081
|
-
for b in assistant_content
|
|
2073
|
+
# tool_use 和 tool_result 被展平为 text
|
|
2074
|
+
assert all(
|
|
2075
|
+
b.get("type") not in ("tool_use", "tool_result") for b in assistant_content
|
|
2082
2076
|
)
|
|
2083
|
-
# 不应插入额外的 user 消息
|
|
2084
|
-
assert len(result["messages"]) == 1
|
|
2085
2077
|
|
|
2086
2078
|
def test_self_cleanup_preserves_srvtoolu_ids(self):
|
|
2087
2079
|
"""回归保护: 自清理通道不得改写 zhipu 原生 srvtoolu_* ID."""
|
|
@@ -268,8 +268,8 @@ class TestCopilotToZhipuChannel:
|
|
|
268
268
|
assert "removed_thinking_param" in adaptations
|
|
269
269
|
assert "removed_extended_thinking_param" in adaptations
|
|
270
270
|
|
|
271
|
-
def
|
|
272
|
-
"""copilot → zhipu
|
|
271
|
+
def test_flattens_tool_use_blocks(self):
|
|
272
|
+
"""copilot → zhipu 将 tool_use 展平为 text 块."""
|
|
273
273
|
body = {
|
|
274
274
|
"messages": [
|
|
275
275
|
{
|
|
@@ -279,7 +279,7 @@ class TestCopilotToZhipuChannel:
|
|
|
279
279
|
"type": "tool_use",
|
|
280
280
|
"id": "toolu_1",
|
|
281
281
|
"name": "bash",
|
|
282
|
-
"input": {},
|
|
282
|
+
"input": {"command": "ls"},
|
|
283
283
|
},
|
|
284
284
|
],
|
|
285
285
|
},
|
|
@@ -290,15 +290,15 @@ class TestCopilotToZhipuChannel:
|
|
|
290
290
|
],
|
|
291
291
|
}
|
|
292
292
|
prepared, adaptations = prepare_copilot_to_zhipu(body)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
293
|
+
assert any("flattened" in a for a in adaptations)
|
|
294
|
+
# tool_use 被转为 text
|
|
295
|
+
assistant_content = prepared["messages"][0]["content"]
|
|
296
|
+
assert all(b.get("type") != "tool_use" for b in assistant_content)
|
|
297
|
+
assert any(
|
|
298
|
+
"Tool Call: bash" in b.get("text", "")
|
|
299
|
+
for b in assistant_content
|
|
300
|
+
if b.get("type") == "text"
|
|
301
|
+
)
|
|
302
302
|
|
|
303
303
|
def test_combined_transformations(self):
|
|
304
304
|
body = {
|
|
@@ -333,14 +333,9 @@ class TestCopilotToZhipuChannel:
|
|
|
333
333
|
# cache_control 保留
|
|
334
334
|
assert "cache_control" in prepared["system"][0]
|
|
335
335
|
assert "thinking" not in prepared
|
|
336
|
-
#
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
b
|
|
340
|
-
for b in user_content
|
|
341
|
-
if isinstance(b, dict) and b.get("type") == "tool_result"
|
|
342
|
-
]
|
|
343
|
-
assert len(tool_results) == 0
|
|
336
|
+
# tool_use 被展平为 text
|
|
337
|
+
assistant_content = prepared["messages"][0]["content"]
|
|
338
|
+
assert all(b.get("type") != "tool_use" for b in assistant_content)
|
|
344
339
|
|
|
345
340
|
def test_preserves_original_body(self):
|
|
346
341
|
body = {
|
|
@@ -397,8 +392,8 @@ class TestCopilotToZhipuChannel:
|
|
|
397
392
|
assert prepared2 == prepared1
|
|
398
393
|
assert adaptations2 == []
|
|
399
394
|
|
|
400
|
-
def
|
|
401
|
-
"""copilot → zhipu
|
|
395
|
+
def test_flattens_tool_result_in_user_message(self):
|
|
396
|
+
"""copilot → zhipu 将 user 消息中的 tool_result 展平为 text."""
|
|
402
397
|
body = {
|
|
403
398
|
"messages": [
|
|
404
399
|
{
|
|
@@ -425,9 +420,10 @@ class TestCopilotToZhipuChannel:
|
|
|
425
420
|
],
|
|
426
421
|
}
|
|
427
422
|
prepared, adaptations = prepare_copilot_to_zhipu(body)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
assert
|
|
423
|
+
# tool_result 被展平为 text
|
|
424
|
+
user_content = prepared["messages"][1]["content"]
|
|
425
|
+
assert all(b.get("type") != "tool_result" for b in user_content)
|
|
426
|
+
assert any("flattened" in a for a in adaptations)
|
|
431
427
|
|
|
432
428
|
|
|
433
429
|
# ── zhipu → anthropic 转换通道测试 ────────────────────────────────
|
|
@@ -764,7 +760,7 @@ class TestZhipuSelfCleanupChannel:
|
|
|
764
760
|
assert any("zhipu_vendor_blocks" in a for a in adaptations)
|
|
765
761
|
|
|
766
762
|
def test_preserves_inline_tool_result_in_assistant(self):
|
|
767
|
-
"""assistant 内联 tool_result
|
|
763
|
+
"""assistant 内联 tool_result 和 tool_use 被展平为 text 块."""
|
|
768
764
|
body = {
|
|
769
765
|
"messages": [
|
|
770
766
|
{
|
|
@@ -788,19 +784,15 @@ class TestZhipuSelfCleanupChannel:
|
|
|
788
784
|
}
|
|
789
785
|
prepared, adaptations = prepare_zhipu_self_cleanup(body)
|
|
790
786
|
|
|
791
|
-
#
|
|
787
|
+
# tool_use 和 tool_result 均被展平为 text
|
|
792
788
|
assistant_content = prepared["messages"][0]["content"]
|
|
793
|
-
assert
|
|
794
|
-
b.get("type")
|
|
795
|
-
for b in assistant_content
|
|
789
|
+
assert all(
|
|
790
|
+
b.get("type") not in ("tool_use", "tool_result") for b in assistant_content
|
|
796
791
|
)
|
|
797
|
-
|
|
798
|
-
assert not any("misplaced" in a for a in adaptations)
|
|
799
|
-
assert not any("orphaned" in a for a in adaptations)
|
|
800
|
-
assert not any("injected" in a for a in adaptations)
|
|
792
|
+
assert any("flattened" in a for a in adaptations)
|
|
801
793
|
|
|
802
|
-
def
|
|
803
|
-
"""
|
|
794
|
+
def test_tool_result_flattened_to_text(self):
|
|
795
|
+
"""自清理通道将 tool_result 展平为 text 块."""
|
|
804
796
|
body = {
|
|
805
797
|
"messages": [
|
|
806
798
|
{
|
|
@@ -827,13 +819,13 @@ class TestZhipuSelfCleanupChannel:
|
|
|
827
819
|
],
|
|
828
820
|
}
|
|
829
821
|
prepared, adaptations = prepare_zhipu_self_cleanup(body)
|
|
822
|
+
# tool_result 被展平为 text
|
|
823
|
+
user_content = prepared["messages"][1]["content"]
|
|
824
|
+
assert all(b.get("type") != "tool_result" for b in user_content)
|
|
825
|
+
assert any("flattened" in a for a in adaptations)
|
|
830
826
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
assert not any("injected" in a for a in adaptations)
|
|
834
|
-
|
|
835
|
-
def test_preserves_existing_id(self):
|
|
836
|
-
"""tool_result 已有 id 字段时应原样保留,不被修改."""
|
|
827
|
+
def test_flattens_tool_result_with_existing_id(self):
|
|
828
|
+
"""自清理通道将含 id 的 tool_result 也展平为 text."""
|
|
837
829
|
body = {
|
|
838
830
|
"messages": [
|
|
839
831
|
{
|
|
@@ -861,9 +853,10 @@ class TestZhipuSelfCleanupChannel:
|
|
|
861
853
|
],
|
|
862
854
|
}
|
|
863
855
|
prepared, adaptations = prepare_zhipu_self_cleanup(body)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
assert
|
|
856
|
+
# tool_result 被展平为 text,不再保留原结构
|
|
857
|
+
user_content = prepared["messages"][1]["content"]
|
|
858
|
+
assert all(b.get("type") != "tool_result" for b in user_content)
|
|
859
|
+
assert any("flattened" in a for a in adaptations)
|
|
867
860
|
|
|
868
861
|
def test_preserves_srvtoolu_ids(self):
|
|
869
862
|
"""zhipu 原生 srvtoolu_* ID 与 server_tool_use 类型必须保留."""
|
|
@@ -1074,25 +1067,25 @@ class TestZhipuSelfCleanupChannel:
|
|
|
1074
1067
|
assistant_content = prepared["messages"][0]["content"]
|
|
1075
1068
|
# delta 被剥离
|
|
1076
1069
|
assert all(b.get("type") != "server_tool_use_delta" for b in assistant_content)
|
|
1077
|
-
#
|
|
1078
|
-
assert any(
|
|
1079
|
-
|
|
1080
|
-
for b in assistant_content
|
|
1081
|
-
)
|
|
1070
|
+
# tool_use / tool_result 被 flatten 为 text 块
|
|
1071
|
+
assert not any(b.get("type") == "tool_use" for b in assistant_content)
|
|
1072
|
+
assert not any(b.get("type") == "tool_result" for b in assistant_content)
|
|
1082
1073
|
# server_tool_use 与其 srvtoolu_* ID 完整保留
|
|
1083
1074
|
srv_block = next(
|
|
1084
1075
|
b for b in assistant_content if b.get("type") == "server_tool_use"
|
|
1085
1076
|
)
|
|
1086
1077
|
assert srv_block["id"] == "srvtoolu_native"
|
|
1087
|
-
# tool_use
|
|
1088
|
-
|
|
1089
|
-
b for b in assistant_content if b.get("type") == "
|
|
1090
|
-
|
|
1091
|
-
assert
|
|
1078
|
+
# flatten 后应包含 tool_use 和 tool_result 对应的 text 块
|
|
1079
|
+
text_contents = [
|
|
1080
|
+
b.get("text", "") for b in assistant_content if b.get("type") == "text"
|
|
1081
|
+
]
|
|
1082
|
+
assert any("Tool Call: bash" in t for t in text_contents)
|
|
1083
|
+
assert any("Tool Result for toolu_bash_001" in t for t in text_contents)
|
|
1092
1084
|
# 不插入额外 user 消息
|
|
1093
1085
|
assert len(prepared["messages"]) == 1
|
|
1094
1086
|
# 关键 adaptation 标签
|
|
1095
1087
|
assert any("zhipu_vendor_blocks" in a for a in adaptations)
|
|
1088
|
+
assert any("flattened" in a and "tool_blocks" in a for a in adaptations)
|
|
1096
1089
|
# 不应有 tool pairing / id 注入 相关 adaptation
|
|
1097
1090
|
assert not any("misplaced" in a for a in adaptations)
|
|
1098
1091
|
assert not any("injected" in a for a in adaptations)
|
|
@@ -2858,8 +2851,8 @@ class TestAnthropicToZhipuChannel:
|
|
|
2858
2851
|
assert any("server_tool_use" in a for a in adaptations)
|
|
2859
2852
|
assert any("thinking_blocks" in a for a in adaptations)
|
|
2860
2853
|
|
|
2861
|
-
def
|
|
2862
|
-
"""anthropic → zhipu
|
|
2854
|
+
def test_flattens_tool_result_in_user_message(self):
|
|
2855
|
+
"""anthropic → zhipu 将 tool_result 展平为 text 块."""
|
|
2863
2856
|
body = {
|
|
2864
2857
|
"messages": [
|
|
2865
2858
|
{
|
|
@@ -2886,6 +2879,7 @@ class TestAnthropicToZhipuChannel:
|
|
|
2886
2879
|
],
|
|
2887
2880
|
}
|
|
2888
2881
|
prepared, adaptations = prepare_anthropic_to_zhipu(body)
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
assert
|
|
2882
|
+
# tool_result 被展平为 text
|
|
2883
|
+
user_content = prepared["messages"][1]["content"]
|
|
2884
|
+
assert all(b.get("type") != "tool_result" for b in user_content)
|
|
2885
|
+
assert any("flattened" in a for a in adaptations)
|