coding-proxy 0.3.1a1__tar.gz → 0.3.1a3__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.1a1 → coding_proxy-0.3.1a3}/CHANGELOG.md +10 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/PKG-INFO +9 -1
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/README.md +8 -0
- coding_proxy-0.3.1a3/docs/issue.md +143 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/zh-CN/README.md +8 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/pyproject.toml +1 -1
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/vendor_channels.py +281 -38
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/error_classifier.py +14 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/executor.py +13 -11
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/usage_parser.py +18 -20
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/server/dashboard.py +3 -3
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_error_classifier.py +38 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_parse_usage.py +45 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_router_executor.py +205 -15
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_vendor_channels.py +1047 -25
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/uv.lock +1 -1
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/.gitignore +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/AGENTS.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/LICENSE +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/assets/dashboard-v0.2.4.png +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_types.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_vendors.py +0 -0
- {coding_proxy-0.3.1a1 → coding_proxy-0.3.1a3}/tests/test_zhipu.py +0 -0
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
- fix(vendor-channels): 新增 zhipu 同 vendor 自清理通道,修复 GLM-5 自循环 400 + tool_results 偶发降级;
|
|
8
|
+
- fix(vendor-channels): 修复 `_rewrite_srvtoolu_ids` 块顺序敏感性导致 inline tool_result 漏改名,进而 enforce 阶段 dict key 与 tool_use_ids 错位、anthropic 报 `tool_use ids without tool_result blocks immediately after` 的 cascade failover 问题(改为两遍扫描:先收集 id_map,再统一改写所有 tool_result.tool_use_id 引用);
|
|
9
|
+
- fix(vendor-channels): `enforce_anthropic_tool_pairing` 增加全局 sanity check pass,主循环边角错位让 dangling tool_use 漏过校验时兜底合成 is_error 占位并打 `pairing_sanity_repaired` 标签,避免 anthropic 二次报错;
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
- fix(vendor-channels): 新增 `anthropic → zhipu` 跨供应商转换通道,修复 Anthropic beta 功能(web search, computer use)产生的 `server_tool_use` 块导致 zhipu 400 错误的问题;
|
|
14
|
+
- fix(error-classifier): 增强语义拒绝检测,识别 zhipu 等供应商返回的中文错误消息(如「API 调用参数有误」code=1210),确保正确触发故障转移;
|
|
15
|
+
- fix(vendor-channels): `_remove_vendor_blocks` 增加空内容占位保护,防止内容块全部剥离后消息结构不合法。
|
|
16
|
+
|
|
7
17
|
## [v0.3.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.3.0) — 2026-04-20
|
|
8
18
|
|
|
9
19
|
> [!IMPORTANT]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1a3
|
|
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
|
|
@@ -113,6 +113,14 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3392
|
|
|
113
113
|
claude
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
### 5. Peek at the Dashboard
|
|
117
|
+
|
|
118
|
+
Curious about your real-time token spend, latency, and circuit breaker pulse? Pop open your browser and head to:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
open http://127.0.0.1:3392/dashboard
|
|
122
|
+
```
|
|
123
|
+
|
|
116
124
|
---
|
|
117
125
|
|
|
118
126
|
## 🛠️ The CLI Console Guide
|
|
@@ -86,6 +86,14 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3392
|
|
|
86
86
|
claude
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### 5. Peek at the Dashboard
|
|
90
|
+
|
|
91
|
+
Curious about your real-time token spend, latency, and circuit breaker pulse? Pop open your browser and head to:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
open http://127.0.0.1:3392/dashboard
|
|
95
|
+
```
|
|
96
|
+
|
|
89
97
|
---
|
|
90
98
|
|
|
91
99
|
## 🛠️ The CLI Console Guide
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Issue 处理档案
|
|
2
|
+
|
|
3
|
+
> 维护已处理过的 Issue 摘要(问题描述、表因根因、处理方式、后续防范、同类问题影响与处理注意事项),便于同类问题的跨上下文处理。识别相同 Issue 时应在原条目追加复盘,避免同 Issue 多处维护。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## streaming usage parse failed: 'NoneType' object has no attribute 'get'
|
|
8
|
+
|
|
9
|
+
**问题描述**
|
|
10
|
+
|
|
11
|
+
OpenAI 兼容 SSE 流式响应过程中,单次请求日志反复刷出数十条 WARNING:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
WARNING streaming usage parse failed: 'NoneType' object has no attribute 'get'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
警告本身被上层 `try/except` 吞掉不影响主链路,但日志噪声严重,且每帧都丢失了 usage 累加。
|
|
18
|
+
|
|
19
|
+
**表因**
|
|
20
|
+
|
|
21
|
+
`StreamingUsageAccumulator.feed` 调用 `parse_usage_from_chunk` 解析 SSE chunk 时抛出 `AttributeError`。
|
|
22
|
+
|
|
23
|
+
**根因**
|
|
24
|
+
|
|
25
|
+
`src/coding/proxy/routing/usage_parser.py::parse_usage_from_chunk` 中 Anthropic message_start 与 Anthropic message_delta / OpenAI 两条分支都使用了脆弱的判空模式:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
if "usage" in data: # 仅判断 key 存在
|
|
29
|
+
u = data["usage"] # 但值可能是 null
|
|
30
|
+
u.get("output_tokens", 0) # AttributeError
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
部分上游(含某些 OpenAI 兼容供应商)在中间 chunk 显式发送 `"usage": null` 占位帧,`in` 检查通过但取出的是 `None`。
|
|
34
|
+
|
|
35
|
+
**处理方式**
|
|
36
|
+
|
|
37
|
+
将两处 guard 统一改为 `u = container.get("usage"); if isinstance(u, dict):`,既排除缺省也排除 null,并顺手移除内部冗余的 `if isinstance(u, dict):` 包装层(已被外层 guard 覆盖)。同时新增三个回归用例覆盖 `data.usage = null` / `message.usage = null` / null 帧后跟有效帧三种场景。
|
|
38
|
+
|
|
39
|
+
**后续防范**
|
|
40
|
+
|
|
41
|
+
- 解析外部 SSE / JSON 结构时, 不要单独使用 `if key in data` 作为安全 guard, 应统一采用 `value = data.get(key); if isinstance(value, dict):` 的双重保护, 同时排除缺省与显式 null。
|
|
42
|
+
- 对 try/except 包裹的 WARNING 路径要保持警觉: 异常被吞不代表无害,重复刷屏的同类警告往往暗示防御性 guard 过窄,需要回溯至根因修复,而非依赖 except 兜底。
|
|
43
|
+
|
|
44
|
+
**同类问题影响与处理注意事项**
|
|
45
|
+
|
|
46
|
+
- 本仓库内 `parse_usage_from_chunk` 的 Gemini `usageMetadata` 分支 (line ~219) 已经使用 `isinstance(um, dict)` 防御, 不受影响, 可作为参考实现。
|
|
47
|
+
- 检查其他解析器 (如 routing / vendor adapter 层) 是否还有 `if "key" in data: v = data["key"]; v.get(...)` 这种模式, 必要时同步加固。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## zhipu 自循环 400 + tool_results 偶发降级
|
|
52
|
+
|
|
53
|
+
**问题描述**
|
|
54
|
+
|
|
55
|
+
生产日志反复出现下述链路: 请求一开始命中 zhipu 主 tier, 但在含 `tool_results` 的多轮工具调用场景下偶发返回 400, 触发到 copilot 二级 tier。具体日志特征:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
WARNING Tier zhipu likely format incompatibility (400 + tool_results), trying next tier without recording failure
|
|
59
|
+
WARNING Tier zhipu semantic rejection (400), trying next tier without recording failure
|
|
60
|
+
DEBUG Applied transition channel zhipu → copilot: rewritten_38_srvtoolu_ids, stripped_16_thinking_blocks, removed_3_cache_control_fields, misplaced_tool_result_relocated
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
zhipu → copilot 通道的 adaptations 列表暴露了上一轮 zhipu 响应中存在的非标准产物 (`srvtoolu_*` ID、自签 thinking、错位的 `tool_result`)。
|
|
64
|
+
|
|
65
|
+
**表因**
|
|
66
|
+
|
|
67
|
+
zhipu 自身偶发返回 400, 但错误体非 JSON 结构, 由 `_is_likely_request_format_error()` 判定为「格式不兼容」并跳过当前 tier。
|
|
68
|
+
|
|
69
|
+
**根因**
|
|
70
|
+
|
|
71
|
+
1. zhipu 是 `NativeAnthropicVendor` 薄透传供应商, **不做任何请求体预处理**。
|
|
72
|
+
2. `executor._determine_source_vendor` 三条优先级路径均以 `source != target_name` 过滤掉了同 vendor 自转换。
|
|
73
|
+
3. `VENDOR_TRANSITIONS` 注册表中无 `("zhipu", "zhipu")` 条目。
|
|
74
|
+
|
|
75
|
+
后果: GLM-5 偶发产出非标准产物 (assistant 内联 `tool_result`、`server_tool_use_delta` 流式残块) 后, Claude Code 把这些产物原样回送下一轮请求时, **没有任何清洗发生**, 直接被转发到 zhipu 自身, 命中 zhipu 端的输入校验返回 400。
|
|
76
|
+
|
|
77
|
+
**处理方式**
|
|
78
|
+
|
|
79
|
+
- 在 `vendor_channels.py` 新增 `prepare_zhipu_self_cleanup` 函数, 仅修复 zhipu 自身拒绝的两类产物:
|
|
80
|
+
1. 剥离 `server_tool_use_delta` 流式残块
|
|
81
|
+
2. `enforce_anthropic_tool_pairing` 把 assistant 内联 `tool_result` 重定位到紧随 user 消息
|
|
82
|
+
- 显式 **保留** zhipu 原生支持的特性: `srvtoolu_*` ID、`server_tool_use` 类型、自签 thinking signature、`cache_control` (cache_read 已在生产实证)、顶层 `thinking` 参数。
|
|
83
|
+
- 在 `VENDOR_TRANSITIONS` 注册 `("zhipu", "zhipu") = prepare_zhipu_self_cleanup`。
|
|
84
|
+
- 在 `executor._determine_source_vendor` 三条优先级路径中, 把「`source != target`」过滤替换为「通道已注册」门控 (`get_transition_channel(...) is not None`), 让自转换通道在显式注册时启用, 未注册时退化为原行为。
|
|
85
|
+
|
|
86
|
+
**后续防范**
|
|
87
|
+
|
|
88
|
+
- 新增 `NativeAnthropicVendor` 子类 (minimax / kimi / doubao / xiaomi / alibaba 等) 时, 若上游 vendor 偶发产出违反 Anthropic 规范的产物, 可按需注册同名自清理通道, executor 无需任何额外改动。
|
|
89
|
+
- 同 vendor 自转换通道应**精确剪裁**: 仅修复 vendor 自身拒绝的产物, 不要套用跨 vendor 通道的全量清理 (会误伤 vendor 原生支持的特性, 如 cache_control 损失带来 cache_read miss)。
|
|
90
|
+
|
|
91
|
+
**同类问题影响与处理注意事项**
|
|
92
|
+
|
|
93
|
+
- `enforce_anthropic_tool_pairing` 仅识别 `tool_use` 类型 (不含 `server_tool_use`), 因为 `server_tool_use` 由 vendor 自身执行, 不需要客户端 `tool_result`。构造测试或类似清洗逻辑时需注意此差别。
|
|
94
|
+
- `_is_likely_request_format_error()` 把「400 + tool_results + 非结构化错误体」一律标记为格式不兼容并跳过 tier 不计熔断器, 这层兜底虽能维持可用性但会**掩盖** vendor 自身的间歇性问题, 让根因更难发现。处理类似 400 偶发时, 应优先看 `Applied transition channel` 日志中的 adaptations 列表, 它能精确暴露上游响应中的非标准产物。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## anthropic 报 messages.X tool_use 缺 tool_result (zhipu→anthropic 故障转移失败)
|
|
99
|
+
|
|
100
|
+
**问题描述**
|
|
101
|
+
|
|
102
|
+
zhipu 完成响应后, executor 故障转移至 anthropic 时反复失败 (HTTP 400):
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
DEBUG Applied transition channel zhipu → anthropic: rewritten_86_srvtoolu_ids, misplaced_tool_result_relocated, stripped_18_thinking_blocks
|
|
106
|
+
WARNING anthropic stream error: status=400 ... messages.3: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_normalized_3.
|
|
107
|
+
INFO Failover: anthropic → zhipu (reason: HTTP 400)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
adaptations 列表显示 `misplaced_tool_result_relocated` 但**没有** `orphaned_tool_use_repaired`, 即 enforce 单遍扫描视角下认为所有 tool_use 都已配对; 但 anthropic 仍报 messages.X 缺 tool_result, 导致请求级 cascade failover 反复回到 zhipu。
|
|
111
|
+
|
|
112
|
+
**表因**
|
|
113
|
+
|
|
114
|
+
`prepare_zhipu_to_anthropic` 链路输出的请求体中, 某个 assistant 的 `tool_use` 在紧邻的 user 消息中没有匹配的 `tool_result` 块。
|
|
115
|
+
|
|
116
|
+
**根因**
|
|
117
|
+
|
|
118
|
+
`_rewrite_srvtoolu_ids` 采用单遍正向扫描: 在同一次循环中一边收集 srvtoolu_* → toolu_normalized_* 的 id_map, 一边改写遇到的 `tool_result.tool_use_id`。GLM-5 流式偶发将 inline tool_result 输出在本消息 `server_tool_use` 之前 (block 顺序异常), 导致:
|
|
119
|
+
|
|
120
|
+
1. 处理 inline tool_result 时, id_map 尚未填入对应 srvtoolu_* → 漏改名, inline 仍保留 `srvtoolu_X`
|
|
121
|
+
2. 处理本消息 server_tool_use 时, 填入 id_map 并把 tool_use 改名为 `toolu_normalized_X`
|
|
122
|
+
3. 进入 `enforce_anthropic_tool_pairing` 时:
|
|
123
|
+
- A 步 extracted dict key = `srvtoolu_X` (inline 保留的旧 ID)
|
|
124
|
+
- B 步 tool_use_ids = `[toolu_normalized_X]` (已改名)
|
|
125
|
+
- F 步 `uid in extracted` 检查失败 (key 错位), 但若 next user 已含其他 stale tool_result 让 existing_result_ids "巧合" 命中, F 步会跳过 synth → 不触发 orphan 标签
|
|
126
|
+
- 最终 anthropic 看到 messages.X 真的缺 toolu_normalized_X 的 tool_result → 400
|
|
127
|
+
|
|
128
|
+
**处理方式**
|
|
129
|
+
|
|
130
|
+
- `_rewrite_srvtoolu_ids` 改为**两遍扫描**: Pass 1 仅遍历 assistant 消息, 收集 id_map 并改写 tool_use 自身的 id 与 type; Pass 2 全量遍历所有消息 (含 user / 异常 assistant 内联), 统一改写所有 `tool_result.tool_use_id` 引用。彻底消除 block 顺序敏感性。
|
|
131
|
+
- `enforce_anthropic_tool_pairing` 主循环结束后追加**全局 sanity check pass**: 重新遍历每条 assistant, 验证其 tool_use_ids 全部在 next user 的 tool_result 中存在; 发现遗漏直接合成 is_error 占位并打 `pairing_sanity_repaired` 标签。作为防御深度抵御未来其他主循环边角错位。
|
|
132
|
+
- A 步对 `tool_use_id` 缺失的破损 inline tool_result 也计入 relocated_count (避免 silent drop 影响 adaptations 标签可观测性)。
|
|
133
|
+
|
|
134
|
+
**后续防范**
|
|
135
|
+
|
|
136
|
+
- 任何"按出现顺序填充字典 + 同遍引用查询"的两阶段操作都应警惕**顺序耦合**问题。两遍扫描 (collect → apply) 是消除此类 bug 的标准 pattern。
|
|
137
|
+
- 关键校验函数应有**主循环 + 全局 sanity check** 的双层结构, 单层校验在边角场景下容易被绕过。
|
|
138
|
+
- 处理 anthropic `tool_use ids without tool_result blocks immediately after` 类 cascade failover 时, **adaptations 标签能否复现日志**是定位 root cause 的强信号: 若 enforce 视角与 anthropic 视角不一致 (有 misplaced 但无 orphan, anthropic 仍报错), 必有上游 _rewrite / id 改写阶段的隐藏漏洞。
|
|
139
|
+
|
|
140
|
+
**同类问题影响与处理注意事项**
|
|
141
|
+
|
|
142
|
+
- 任何对 messages 进行 ID 重写的转换链 (如 `_rewrite_srvtoolu_ids`、`anthropic_to_openai`、`anthropic_to_gemini`) 都应使用两遍扫描或一次性收集后再批量改写, 以保证 block 顺序无关性。
|
|
143
|
+
- enforce 类校验函数若依赖 dict key 与 list 元素的**等同性**, 必须先确保两者在同一参考系下 (改名前 vs 改名后); 否则错位会以 "看起来 OK 实际有漏" 的方式静默泄漏到下游。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1a3"
|
|
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"
|
|
@@ -10,6 +10,8 @@ executor 层通过 ``get_transition_channel()`` 查表分发,无需感知具
|
|
|
10
10
|
zhipu → anthropic : prepare_zhipu_to_anthropic (剥离 thinking + tool pairing)
|
|
11
11
|
zhipu → copilot : prepare_zhipu_to_copilot (剥离 thinking + cache_control + tool pairing)
|
|
12
12
|
copilot → zhipu : prepare_copilot_to_zhipu (剥离 thinking + cache_control + 移除 thinking 参数 + tool pairing)
|
|
13
|
+
zhipu → zhipu : prepare_zhipu_self_cleanup (剥离 server_tool_use_delta + tool pairing)
|
|
14
|
+
anthropic → zhipu : prepare_anthropic_to_zhipu (剥离 server_tool_use + thinking + cache_control + 移除 thinking 参数 + tool pairing)
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
from __future__ import annotations
|
|
@@ -108,6 +110,10 @@ def enforce_anthropic_tool_pairing(
|
|
|
108
110
|
|
|
109
111
|
此函数是一个**自包含的单遍处理**,不依赖 Phase 1 收集的 misplaced 信息。
|
|
110
112
|
|
|
113
|
+
最终在主循环之后执行一次幂等的全局 sanity check pass, 防御主循环的边角
|
|
114
|
+
错位 (如 inline tool_result 引用未在本消息出现的 tool_use_id, 导致 extracted
|
|
115
|
+
字典 key 与 tool_use_ids 集合错位) 让 dangling tool_use 漏过校验。
|
|
116
|
+
|
|
111
117
|
Args:
|
|
112
118
|
messages_list: 消息列表(就地修改)。
|
|
113
119
|
|
|
@@ -139,10 +145,13 @@ def enforce_anthropic_tool_pairing(
|
|
|
139
145
|
if tid:
|
|
140
146
|
extracted_tool_results[tid] = block
|
|
141
147
|
relocated_count += 1
|
|
148
|
+
else:
|
|
149
|
+
# 缺 tool_use_id 的破损 tool_result 也视作错位剥离
|
|
150
|
+
relocated_count += 1
|
|
142
151
|
else:
|
|
143
152
|
retained_content.append(block)
|
|
144
153
|
|
|
145
|
-
if extracted_tool_results:
|
|
154
|
+
if extracted_tool_results or len(retained_content) != len(content):
|
|
146
155
|
msg["content"] = retained_content
|
|
147
156
|
|
|
148
157
|
# B. 收集所有 tool_use ID
|
|
@@ -207,10 +216,17 @@ def enforce_anthropic_tool_pairing(
|
|
|
207
216
|
|
|
208
217
|
i += 1
|
|
209
218
|
|
|
219
|
+
# G. 最终全局 sanity check pass (抽出为独立函数便于单测验证正向兜底路径).
|
|
220
|
+
sanity_synthesized = _enforce_pairing_sanity_pass(messages_list)
|
|
221
|
+
|
|
210
222
|
if relocated_count:
|
|
211
223
|
adaptations.append("misplaced_tool_result_relocated")
|
|
212
|
-
if synthesized_ids:
|
|
224
|
+
if synthesized_ids or sanity_synthesized:
|
|
213
225
|
adaptations.append("orphaned_tool_use_repaired")
|
|
226
|
+
|
|
227
|
+
# 主循环 F 段与 sanity G 段分别打日志, 避免 main=0/sanity=N 时把 sanity
|
|
228
|
+
# 兜底误归因为主循环工作 (运维在线日志聚合时易混淆 cross-pass id-map drift).
|
|
229
|
+
if synthesized_ids:
|
|
214
230
|
logger.warning(
|
|
215
231
|
"Vendor degradation adaptation: synthesized %d tool_result block(s) "
|
|
216
232
|
"for orphaned tool_use to satisfy Anthropic pairing constraint. "
|
|
@@ -218,10 +234,94 @@ def enforce_anthropic_tool_pairing(
|
|
|
218
234
|
len(synthesized_ids),
|
|
219
235
|
", ".join(synthesized_ids),
|
|
220
236
|
)
|
|
237
|
+
if sanity_synthesized:
|
|
238
|
+
adaptations.append("pairing_sanity_repaired")
|
|
239
|
+
logger.warning(
|
|
240
|
+
"Pairing sanity check repaired %d dangling tool_use(s) missed by "
|
|
241
|
+
"main pass (likely cross-pass id-map drift). Affected tool_use_ids: %s",
|
|
242
|
+
len(sanity_synthesized),
|
|
243
|
+
", ".join(sanity_synthesized),
|
|
244
|
+
)
|
|
221
245
|
|
|
222
246
|
return adaptations
|
|
223
247
|
|
|
224
248
|
|
|
249
|
+
def _enforce_pairing_sanity_pass(messages_list: list[Any]) -> list[str]:
|
|
250
|
+
"""全局 sanity check pass: 防御主循环边角错位让 dangling tool_use 漏过.
|
|
251
|
+
|
|
252
|
+
例如: extracted dict key 与 _rewrite 后的 tool_use_ids 错位、user_msg
|
|
253
|
+
中已有 stale tool_result 让 F 步误判 existing 命中等场景。
|
|
254
|
+
|
|
255
|
+
扫描所有 assistant 消息, 验证每个 ``tool_use`` block ID 在紧随的 user 消息
|
|
256
|
+
中均存在对应 ``tool_result``; 漏掉的合成 ``is_error`` 占位。
|
|
257
|
+
|
|
258
|
+
抽取为独立函数的目的: 主循环 F 步在当前实现下能覆盖所有 dangling tool_use,
|
|
259
|
+
导致 sanity 实际兜底分支在公开 API 测试中无法被触发; 独立函数便于直接
|
|
260
|
+
构造「绕过主循环」的输入, 对兜底合成路径建立正向回归保护。
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
messages_list: 消息列表 (就地修改, 必要时插入空 user 消息).
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
sanity 兜底合成的 tool_use_id 列表 (空表示主循环已完成所有配对).
|
|
267
|
+
"""
|
|
268
|
+
sanity_synthesized: list[str] = []
|
|
269
|
+
j = 0
|
|
270
|
+
while j < len(messages_list):
|
|
271
|
+
msg_j = messages_list[j]
|
|
272
|
+
if not isinstance(msg_j, dict) or msg_j.get("role") != "assistant":
|
|
273
|
+
j += 1
|
|
274
|
+
continue
|
|
275
|
+
content_j = msg_j.get("content")
|
|
276
|
+
if not isinstance(content_j, list):
|
|
277
|
+
j += 1
|
|
278
|
+
continue
|
|
279
|
+
tu_ids = [
|
|
280
|
+
b["id"]
|
|
281
|
+
for b in content_j
|
|
282
|
+
if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("id")
|
|
283
|
+
]
|
|
284
|
+
if not tu_ids:
|
|
285
|
+
j += 1
|
|
286
|
+
continue
|
|
287
|
+
next_j = j + 1
|
|
288
|
+
if (
|
|
289
|
+
next_j < len(messages_list)
|
|
290
|
+
and isinstance(messages_list[next_j], dict)
|
|
291
|
+
and messages_list[next_j].get("role") == "user"
|
|
292
|
+
):
|
|
293
|
+
next_user = messages_list[next_j]
|
|
294
|
+
else:
|
|
295
|
+
next_user = {"role": "user", "content": []}
|
|
296
|
+
messages_list.insert(next_j, next_user)
|
|
297
|
+
nu_content = next_user.get("content")
|
|
298
|
+
if isinstance(nu_content, str):
|
|
299
|
+
next_user["content"] = [{"type": "text", "text": nu_content}]
|
|
300
|
+
elif not isinstance(nu_content, list):
|
|
301
|
+
next_user["content"] = []
|
|
302
|
+
nu_result_ids = {
|
|
303
|
+
b["tool_use_id"]
|
|
304
|
+
for b in next_user["content"]
|
|
305
|
+
if isinstance(b, dict)
|
|
306
|
+
and b.get("type") == "tool_result"
|
|
307
|
+
and b.get("tool_use_id")
|
|
308
|
+
}
|
|
309
|
+
for uid in tu_ids:
|
|
310
|
+
if uid in nu_result_ids:
|
|
311
|
+
continue
|
|
312
|
+
next_user["content"].append(
|
|
313
|
+
{
|
|
314
|
+
"type": "tool_result",
|
|
315
|
+
"tool_use_id": uid,
|
|
316
|
+
"content": "",
|
|
317
|
+
"is_error": True,
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
sanity_synthesized.append(uid)
|
|
321
|
+
j += 1
|
|
322
|
+
return sanity_synthesized
|
|
323
|
+
|
|
324
|
+
|
|
225
325
|
def _strip_cache_control(body: dict[str, Any]) -> int:
|
|
226
326
|
"""从 system/messages/tools 中移除 cache_control 字段(就地).
|
|
227
327
|
|
|
@@ -284,7 +384,13 @@ def _remove_vendor_blocks(body: dict[str, Any], block_types: set[str]) -> int:
|
|
|
284
384
|
removed += 1
|
|
285
385
|
continue
|
|
286
386
|
new_content.append(block)
|
|
287
|
-
if
|
|
387
|
+
if content != new_content:
|
|
388
|
+
if not new_content:
|
|
389
|
+
new_content = [{"type": "text", "text": "[vendor_block_removed]"}]
|
|
390
|
+
logger.info(
|
|
391
|
+
"Inserted placeholder text block after stripping "
|
|
392
|
+
"vendor blocks to avoid empty message content",
|
|
393
|
+
)
|
|
288
394
|
message["content"] = new_content
|
|
289
395
|
return removed
|
|
290
396
|
|
|
@@ -294,8 +400,12 @@ def _rewrite_srvtoolu_ids(body: dict[str, Any]) -> tuple[int, dict[str, str]]:
|
|
|
294
400
|
|
|
295
401
|
Anthropic API 要求 tool_use 类型与 ``toolu_*`` 格式的 ID。Zhipu 的
|
|
296
402
|
``server_tool_use`` + ``srvtoolu_*`` 在上游 Anthropic 兼容端点可用,但无法
|
|
297
|
-
|
|
298
|
-
|
|
403
|
+
透传至其他供应商;同时还需重写所有 ``tool_result.tool_use_id`` 引用,
|
|
404
|
+
保持配对关系。
|
|
405
|
+
|
|
406
|
+
采用**两遍扫描**避免块顺序敏感性: GLM-5 偶发将 inline tool_result 输出在
|
|
407
|
+
本消息 tool_use 之前, 单遍扫描会因 id_map 尚未填入而漏改 inline tool_result
|
|
408
|
+
的 tool_use_id, 导致后续 enforce 步骤无法将其与 tool_use 配对。
|
|
299
409
|
|
|
300
410
|
Returns:
|
|
301
411
|
(rewritten_count, id_map) — 重写次数与 {原 ID: 新 ID} 映射。
|
|
@@ -308,45 +418,59 @@ def _rewrite_srvtoolu_ids(body: dict[str, Any]) -> tuple[int, dict[str, str]]:
|
|
|
308
418
|
counter += 1
|
|
309
419
|
return f"toolu_normalized_{counter}"
|
|
310
420
|
|
|
421
|
+
# Pass 1: 收集所有 assistant tool_use / server_tool_use 的 ID 映射
|
|
422
|
+
# 不修改 tool_result, 仅建立 id_map; 同时改写 tool_use 自身的 id 与 type
|
|
311
423
|
for message in body.get("messages", []):
|
|
312
424
|
if not isinstance(message, dict):
|
|
313
425
|
continue
|
|
314
426
|
content = message.get("content")
|
|
315
427
|
if not isinstance(content, list):
|
|
316
428
|
continue
|
|
317
|
-
|
|
429
|
+
if message.get("role") != "assistant":
|
|
430
|
+
continue
|
|
318
431
|
for block in content:
|
|
319
432
|
if not isinstance(block, dict):
|
|
320
433
|
continue
|
|
321
434
|
block_type = block.get("type")
|
|
322
435
|
block_id = block.get("id")
|
|
436
|
+
if block_type not in {"tool_use", "server_tool_use"}:
|
|
437
|
+
continue
|
|
323
438
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if
|
|
439
|
+
if isinstance(block_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
|
|
440
|
+
block_id
|
|
441
|
+
):
|
|
442
|
+
new_id = next_id()
|
|
443
|
+
id_map[block_id] = new_id
|
|
444
|
+
block["id"] = new_id
|
|
445
|
+
block["type"] = "tool_use"
|
|
446
|
+
elif (
|
|
447
|
+
isinstance(block_id, str)
|
|
448
|
+
and block_id
|
|
449
|
+
and not _ANTHROPIC_TOOL_USE_ID_RE.match(block_id)
|
|
450
|
+
and block.get("name")
|
|
451
|
+
):
|
|
452
|
+
# 非标准 ID(非 toolu_ / srvtoolu_),且具备 name 可改写
|
|
453
|
+
new_id = next_id()
|
|
454
|
+
id_map[block_id] = new_id
|
|
455
|
+
block["id"] = new_id
|
|
456
|
+
block["type"] = "tool_use"
|
|
457
|
+
elif block_type == "server_tool_use" and isinstance(block_id, str):
|
|
458
|
+
# 兜底: 类型是 server_tool_use 但 ID 已是标准 toolu_ 形式,仅纠正类型
|
|
459
|
+
block["type"] = "tool_use"
|
|
460
|
+
|
|
461
|
+
# Pass 2: 全量同步所有 tool_result.tool_use_id 引用 (含 user/assistant 内联)
|
|
462
|
+
if id_map:
|
|
463
|
+
for message in body.get("messages", []):
|
|
464
|
+
if not isinstance(message, dict):
|
|
465
|
+
continue
|
|
466
|
+
content = message.get("content")
|
|
467
|
+
if not isinstance(content, list):
|
|
468
|
+
continue
|
|
469
|
+
for block in content:
|
|
470
|
+
if not isinstance(block, dict):
|
|
471
|
+
continue
|
|
472
|
+
if block.get("type") != "tool_result":
|
|
473
|
+
continue
|
|
350
474
|
tool_use_id = block.get("tool_use_id")
|
|
351
475
|
if isinstance(tool_use_id, str) and tool_use_id in id_map:
|
|
352
476
|
block["tool_use_id"] = id_map[tool_use_id]
|
|
@@ -358,8 +482,9 @@ def infer_source_vendor_from_body(body: dict[str, Any]) -> str | None:
|
|
|
358
482
|
"""从请求 body 内容推断源供应商(仅在无会话上下文时作为兜底).
|
|
359
483
|
|
|
360
484
|
启发式(按置信度排序):
|
|
361
|
-
- 出现 ``srvtoolu_*`` 格式的
|
|
362
|
-
- 出现 ``
|
|
485
|
+
- 出现 ``srvtoolu_*`` 格式的 ID → zhipu
|
|
486
|
+
- 出现 ``server_tool_use_delta`` 类型的 content block → zhipu
|
|
487
|
+
- 出现 ``server_tool_use`` 块 + ``toolu_*`` ID → anthropic(beta 功能产物)
|
|
363
488
|
|
|
364
489
|
原则: 只读扫描不修改 body;无匹配返回 None(视作纯净无需跨供应商清洗)。
|
|
365
490
|
|
|
@@ -367,7 +492,7 @@ def infer_source_vendor_from_body(body: dict[str, Any]) -> str | None:
|
|
|
367
492
|
body: Anthropic Messages 请求体。
|
|
368
493
|
|
|
369
494
|
Returns:
|
|
370
|
-
|
|
495
|
+
推断的源供应商名称(``"zhipu"`` 或 ``"anthropic"``),无法推断返回 None。
|
|
371
496
|
"""
|
|
372
497
|
for message in body.get("messages", []):
|
|
373
498
|
if not isinstance(message, dict):
|
|
@@ -379,18 +504,35 @@ def infer_source_vendor_from_body(body: dict[str, Any]) -> str | None:
|
|
|
379
504
|
if not isinstance(block, dict):
|
|
380
505
|
continue
|
|
381
506
|
block_type = block.get("type")
|
|
382
|
-
if block_type in _ZHIPU_SERVER_TOOL_USE_TYPES:
|
|
383
|
-
return "zhipu"
|
|
384
507
|
block_id = block.get("id")
|
|
508
|
+
tool_use_id = block.get("tool_use_id")
|
|
509
|
+
|
|
510
|
+
# Zhipu: server_tool_use_delta 是 zhipu 私有流式块(无歧义)
|
|
511
|
+
if block_type == "server_tool_use_delta":
|
|
512
|
+
return "zhipu"
|
|
513
|
+
|
|
514
|
+
# srvtoolu_* ID(无论 block type)→ zhipu
|
|
385
515
|
if isinstance(block_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
|
|
386
516
|
block_id
|
|
387
517
|
):
|
|
388
518
|
return "zhipu"
|
|
389
|
-
tool_use_id = block.get("tool_use_id")
|
|
390
519
|
if isinstance(tool_use_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
|
|
391
520
|
tool_use_id
|
|
392
521
|
):
|
|
393
522
|
return "zhipu"
|
|
523
|
+
|
|
524
|
+
# server_tool_use 块 + toolu_* ID → Anthropic beta 功能
|
|
525
|
+
if (
|
|
526
|
+
block_type == "server_tool_use"
|
|
527
|
+
and isinstance(block_id, str)
|
|
528
|
+
and _ANTHROPIC_TOOL_USE_ID_RE.match(block_id)
|
|
529
|
+
):
|
|
530
|
+
return "anthropic"
|
|
531
|
+
|
|
532
|
+
# server_tool_use 块 + 非 toolu_/srvtoolu_ ID → 按类型兜底归 zhipu
|
|
533
|
+
if block_type == "server_tool_use":
|
|
534
|
+
return "zhipu"
|
|
535
|
+
|
|
394
536
|
return None
|
|
395
537
|
|
|
396
538
|
|
|
@@ -438,6 +580,61 @@ def prepare_copilot_to_zhipu(
|
|
|
438
580
|
return prepared, adaptations
|
|
439
581
|
|
|
440
582
|
|
|
583
|
+
# ── anthropic → zhipu 转换通道 ────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
# Anthropic beta 特有的 server_tool_use 块类型(web search, computer use 等).
|
|
586
|
+
# 这些块在 Anthropic API 中有效,但 zhipu GLM-5 的兼容端点不支持。
|
|
587
|
+
# 注意: 这与 zhipu 自己的 server_tool_use(使用 srvtoolu_* ID)是不同的概念,
|
|
588
|
+
# 但它们共用同一个 type 名称 "server_tool_use"。
|
|
589
|
+
_ANTHROPIC_BETA_BLOCK_TYPES = {"server_tool_use"}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def prepare_anthropic_to_zhipu(
|
|
593
|
+
body: dict[str, Any],
|
|
594
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
595
|
+
"""anthropic → zhipu 转换: 清理 anthropic 产物以适配 GLM-5.
|
|
596
|
+
|
|
597
|
+
Anthropic API 可能产生的非兼容产物:
|
|
598
|
+
- ``server_tool_use`` blocks(web search / computer use 等 beta 功能)
|
|
599
|
+
- ``thinking`` / ``redacted_thinking`` blocks(含 Anthropic 签发的 signature)
|
|
600
|
+
- ``cache_control`` 字段
|
|
601
|
+
- 顶层 ``thinking`` / ``extended_thinking`` 参数
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
(prepared_body, adaptations) — adaptations 为应用的变换描述列表。
|
|
605
|
+
"""
|
|
606
|
+
prepared = copy.deepcopy(body)
|
|
607
|
+
adaptations: list[str] = []
|
|
608
|
+
|
|
609
|
+
# Step 1: 剥离 anthropic 的 server_tool_use blocks(web search, computer use 等)
|
|
610
|
+
removed_stu = _remove_vendor_blocks(prepared, _ANTHROPIC_BETA_BLOCK_TYPES)
|
|
611
|
+
if removed_stu:
|
|
612
|
+
adaptations.append(f"removed_{removed_stu}_server_tool_use_blocks")
|
|
613
|
+
|
|
614
|
+
# Step 2: 剥离 thinking/redacted_thinking blocks
|
|
615
|
+
stripped = strip_thinking_blocks(prepared)
|
|
616
|
+
if stripped:
|
|
617
|
+
adaptations.append(f"stripped_{stripped}_thinking_blocks")
|
|
618
|
+
|
|
619
|
+
# Step 3: 移除 cache_control 字段
|
|
620
|
+
removed_cc = _strip_cache_control(prepared)
|
|
621
|
+
if removed_cc:
|
|
622
|
+
adaptations.append(f"removed_{removed_cc}_cache_control_fields")
|
|
623
|
+
|
|
624
|
+
# Step 4: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
|
|
625
|
+
for param in ("thinking", "extended_thinking"):
|
|
626
|
+
if param in prepared:
|
|
627
|
+
del prepared[param]
|
|
628
|
+
adaptations.append(f"removed_{param}_param")
|
|
629
|
+
|
|
630
|
+
# Step 5: 强制 tool_use/tool_result 配对
|
|
631
|
+
pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
|
|
632
|
+
if pairing_fixes:
|
|
633
|
+
adaptations.extend(pairing_fixes)
|
|
634
|
+
|
|
635
|
+
return prepared, adaptations
|
|
636
|
+
|
|
637
|
+
|
|
441
638
|
# ── zhipu → copilot 转换通道 ─────────────────────────────────────
|
|
442
639
|
|
|
443
640
|
|
|
@@ -539,8 +736,54 @@ def prepare_zhipu_to_anthropic(
|
|
|
539
736
|
return prepared, adaptations
|
|
540
737
|
|
|
541
738
|
|
|
739
|
+
# ── zhipu → zhipu 自清理通道 ──────────────────────────────────────
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def prepare_zhipu_self_cleanup(
|
|
743
|
+
body: dict[str, Any],
|
|
744
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
745
|
+
"""zhipu → zhipu 自清理: 仅修复 zhipu 自身无法消化的产物.
|
|
746
|
+
|
|
747
|
+
GLM-5 偶发地在 assistant 消息中输出 ``tool_result`` 块(违反 Anthropic 规范),
|
|
748
|
+
或在流式响应中暴露 ``server_tool_use_delta`` 私有块。当 Claude Code 将这些
|
|
749
|
+
产物原样回送下一轮请求时,zhipu 的 Anthropic 兼容端点会以 400 拒绝
|
|
750
|
+
(表现为 "400 + tool_results" 偶发,进而触发到 copilot 的降级)。
|
|
751
|
+
|
|
752
|
+
本通道仅修复 zhipu 自身拒绝的两类产物,**保留** 所有 zhipu 原生支持的特性:
|
|
753
|
+
|
|
754
|
+
- ✓ ``srvtoolu_*`` ID 与 ``server_tool_use`` 类型(zhipu 原生)
|
|
755
|
+
- ✓ thinking blocks 的 zhipu 自签 signature
|
|
756
|
+
- ✓ ``cache_control`` 字段(GLM Anthropic 端点支持,cache_read 已实证)
|
|
757
|
+
- ✓ 顶层 ``thinking`` / ``extended_thinking`` 参数
|
|
758
|
+
|
|
759
|
+
清理操作(顺序、就地、幂等):
|
|
760
|
+
1. 剥离 ``server_tool_use_delta`` 流式残块
|
|
761
|
+
2. 强制 tool_use/tool_result 配对(关键: 把 assistant 内联的 tool_result
|
|
762
|
+
搬迁到紧随的 user 消息)
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
(prepared_body, adaptations) — adaptations 为应用的变换描述列表。
|
|
766
|
+
"""
|
|
767
|
+
prepared = copy.deepcopy(body)
|
|
768
|
+
adaptations: list[str] = []
|
|
769
|
+
|
|
770
|
+
# Step 1: 剥离 zhipu 私有流式块类型(input 中不应出现)
|
|
771
|
+
removed_vendor_blocks = _remove_vendor_blocks(prepared, _ZHIPU_VENDOR_BLOCK_TYPES)
|
|
772
|
+
if removed_vendor_blocks:
|
|
773
|
+
adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
|
|
774
|
+
|
|
775
|
+
# Step 2: 强制 tool_use/tool_result 配对
|
|
776
|
+
pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
|
|
777
|
+
if pairing_fixes:
|
|
778
|
+
adaptations.extend(pairing_fixes)
|
|
779
|
+
|
|
780
|
+
return prepared, adaptations
|
|
781
|
+
|
|
782
|
+
|
|
542
783
|
# ── 注册所有转换通道 ──────────────────────────────────────────────
|
|
543
784
|
|
|
544
785
|
VENDOR_TRANSITIONS[("zhipu", "anthropic")] = prepare_zhipu_to_anthropic
|
|
545
786
|
VENDOR_TRANSITIONS[("zhipu", "copilot")] = prepare_zhipu_to_copilot
|
|
546
787
|
VENDOR_TRANSITIONS[("copilot", "zhipu")] = prepare_copilot_to_zhipu
|
|
788
|
+
VENDOR_TRANSITIONS[("zhipu", "zhipu")] = prepare_zhipu_self_cleanup
|
|
789
|
+
VENDOR_TRANSITIONS[("anthropic", "zhipu")] = prepare_anthropic_to_zhipu
|