coding-proxy 0.3.1a10__tar.gz → 0.4.1a1__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.1a10 → coding_proxy-0.4.1a1}/.gitignore +3 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/CHANGELOG.md +19 -8
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/PKG-INFO +2 -2
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/README.md +1 -1
- coding_proxy-0.4.1a1/assets/dashboard-v0.4.0.png +0 -0
- coding_proxy-0.4.1a1/assets/session-v0.4.0.png +0 -0
- coding_proxy-0.4.1a1/docs/issue.md +134 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/zh-CN/README.md +1 -1
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/pyproject.toml +5 -2
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/vendor_channels.py +161 -31
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/handler.py +11 -2
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/operation.py +8 -7
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/server/dashboard.py +164 -61
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/server/routes.py +3 -2
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/antigravity.py +35 -16
- coding_proxy-0.4.1a1/tests/e2e/__init__.py +0 -0
- coding_proxy-0.4.1a1/tests/e2e/conftest.py +199 -0
- coding_proxy-0.4.1a1/tests/e2e/test_e2e_http.py +263 -0
- coding_proxy-0.4.1a1/tests/e2e/test_e2e_token.py +93 -0
- coding_proxy-0.4.1a1/tests/e2e/test_e2e_vendor.py +327 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_antigravity.py +8 -9
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_app_routes.py +70 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_native_api_handler.py +71 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_native_api_operation.py +17 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_vendor_channels.py +418 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/uv.lock +1 -1
- coding_proxy-0.3.1a10/assets/dashboard-v0.2.4.png +0 -0
- coding_proxy-0.3.1a10/docs/issue.md +0 -47
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/AGENTS.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/LICENSE +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_router_executor.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_types.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_vendors.py +0 -0
- {coding_proxy-0.3.1a10 → coding_proxy-0.4.1a1}/tests/test_zhipu.py +0 -0
|
@@ -4,16 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- fix(vendor-channels): 新增 zhipu 同 vendor 自清理通道,修复 GLM-5 自循环 400 + tool_results 偶发降级;
|
|
9
|
-
- 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 引用);
|
|
10
|
-
- fix(vendor-channels): `enforce_anthropic_tool_pairing` 增加全局 sanity check pass,主循环边角错位让 dangling tool_use 漏过校验时兜底合成 is_error 占位并打 `pairing_sanity_repaired` 标签,避免 anthropic 二次报错;
|
|
7
|
+
## [v0.4.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.4.0) — 2026-05-01
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
>
|
|
11
|
+
> **🚀 Session 级专属路由策略!**
|
|
12
|
+
>
|
|
13
|
+
> 给每个 Session 指定专属的 vendor,动态调节不同 vendors 间的 LLM 流量。
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### ✨ 核心亮点
|
|
18
|
+
|
|
19
|
+
- feat(session-policy): 新增 Session 级专属路由策略 (#219)
|
|
20
|
+
- feat(dashboard): 新增会话活动面板 (#222)
|
|
21
|
+
|
|
22
|
+
### 🔧 更多特性
|
|
13
23
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- fix(
|
|
24
|
+
- refactor(logging): 移除已被 ModelCall 汇总行覆盖的冗余 DEBUG 日志 (#203)
|
|
25
|
+
- style(dashboard): 加宽图表 tooltip 令模型名称与用量值单行显示 (#211)
|
|
26
|
+
- fix(usage-parser): 补充 OpenAI/Gemini SSE 流式分支的 model_served 提取 (#214)
|
|
27
|
+
- fix(usage-parser): 兼容 SSE chunk 中 usage 字段为 null 的极端格式 (#212)
|
|
17
28
|
|
|
18
29
|
## [v0.3.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.3.0) — 2026-04-20
|
|
19
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1a1
|
|
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
|
|
@@ -57,7 +57,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
|
|
|
57
57
|
## 🌟 Core Features
|
|
58
58
|
|
|
59
59
|
<div align="center">
|
|
60
|
-
<img src="assets/dashboard-v0.
|
|
60
|
+
<img src="assets/dashboard-v0.4.0.png">
|
|
61
61
|
</div>
|
|
62
62
|
|
|
63
63
|
- **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Google Antigravity, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
|
|
@@ -30,7 +30,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
|
|
|
30
30
|
## 🌟 Core Features
|
|
31
31
|
|
|
32
32
|
<div align="center">
|
|
33
|
-
<img src="assets/dashboard-v0.
|
|
33
|
+
<img src="assets/dashboard-v0.4.0.png">
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
- **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Google Antigravity, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
## anthropic 400: `tool_use` ids were found without `tool_result` blocks immediately after
|
|
52
|
+
|
|
53
|
+
**问题描述**
|
|
54
|
+
|
|
55
|
+
zhipu → anthropic 通道流式请求偶发 400, 错误形如:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
WARNING anthropic stream error: status=400 body=...
|
|
59
|
+
messages.3: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_normalized_2.
|
|
60
|
+
INFO Failover: anthropic → zhipu (reason: HTTP 400)
|
|
61
|
+
INFO Tier zhipu stream succeeded (took over from failed tier: anthropic)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
同一请求伴随 `Applied transition channel zhipu → anthropic: rewritten_N_srvtoolu_ids, misplaced_tool_result_relocated, stripped_M_thinking_blocks` 的 adaptations 但**没有 `orphaned_tool_use_repaired`**, 即转换层主观上认为已配对、但 Anthropic 仍判定结构不合规。Failover 至 zhipu 后请求成功, 证明上游消息体本身没有损坏, 问题出在 zhipu→anthropic 通道转换过程引入了不一致。
|
|
65
|
+
|
|
66
|
+
**表因**
|
|
67
|
+
|
|
68
|
+
`src/coding/proxy/convert/vendor_channels.py::_rewrite_srvtoolu_ids` 在单遍循环中同时承担 Case A (assistant 端 `server_tool_use` → `tool_use` 与 `srvtoolu_*` ID 重写) 与 Case B (任意位置 `tool_result.tool_use_id` 同步重写)。Case B 依赖 `id_map` 已被 Case A 填入。
|
|
69
|
+
|
|
70
|
+
**根因**
|
|
71
|
+
|
|
72
|
+
Zhipu GLM-5 流式响应偶发将 inline `tool_result` 块输出在**对应的 `server_tool_use` 块之前** (同 assistant content 内乱序), 或将 `tool_result` 放在更早的 user 消息中而对应 `tool_use` 在更晚的 assistant 消息。两种乱序下, 单遍扫描遍历到 `tool_result` 时 `id_map` 还是空 → `tool_result.tool_use_id` 不被改写, 停留在 `srvtoolu_X`; 随后 Case A 把对应 `tool_use.id` 改写为 `toolu_normalized_N`。
|
|
73
|
+
|
|
74
|
+
后续 `enforce_anthropic_tool_pairing` Step A 提取这条 misplaced tool_result 时使用**旧 ID** 作为 `extracted_tool_results` 字典 key, Step F 用新 ID 去查 → 不命中 → 走 `existing_result_ids` 分支, 因为相邻 user 的 tool_result 已经被改写到新 ID, 该 uid 命中 `existing_result_ids` 被 continue 跳过, 于是 enforce 错误地认为完成配对、不产生 `orphaned_tool_use_repaired` 标签, 而被默默丢弃的 misplaced tool_result 本应填补到的 user 槽位实际上**仍然缺位**。最终 body 中某条 assistant 的 tool_use 在下一条 user 中找不到对应 tool_result → Anthropic 400。
|
|
75
|
+
|
|
76
|
+
**处理方式**
|
|
77
|
+
|
|
78
|
+
1. `_rewrite_srvtoolu_ids` 改为**两遍扫描**: Pass 1 仅遍历 assistant 消息收集 `id_map` (按 assistant 出现顺序分配, 保持序号兼容性); Pass 2 全量遍历改写任意 `tool_result.tool_use_id`。以"先建表、后改写"的次序消除时序耦合。
|
|
79
|
+
2. 在 `enforce_anthropic_tool_pairing` 主循环末尾追加独立 helper `_enforce_pairing_sanity_pass`, 仅做检测+合成 `is_error=True` 占位 (不剥离、不重定位), 命中追加 `pairing_sanity_repaired` adaptation 并打 WARNING (含 message index 与 uid)。这层作为纵深防御, 在主循环未来重构时仍能稳定守住 Anthropic 配对约束。
|
|
80
|
+
3. 新增回归测试覆盖三类场景: 同 assistant content 内乱序、跨消息边界 tool_result 早于 tool_use、端到端复现日志故障形态。新增 `TestEnforcePairingSanityPass` 独立测试套件确保兜底分支具备正向回归保护。
|
|
81
|
+
|
|
82
|
+
**后续防范**
|
|
83
|
+
|
|
84
|
+
- 任何在多 content block 之间存在**前向引用** (后出现的块定义的标识符被前面的块引用) 的就地改写逻辑, 都必须采用两遍扫描或全局表先建后用, 不可依赖遍历位置上 "上一次循环已经写入" 的隐含次序。
|
|
85
|
+
- 纵深防御层 (sanity helper) 必须**独立可单测**, 而不是把 sanity 内嵌在主路径内部 — 否则主路径的快速通道会让 sanity 分支永远走不到正向测试, 缺乏回归保护。
|
|
86
|
+
- adaptations 标签 (`pairing_sanity_repaired`) 与主循环标签 (`orphaned_tool_use_repaired`) 分离, 便于运维聚合时按层归因。
|
|
87
|
+
|
|
88
|
+
**同类问题影响与处理注意事项**
|
|
89
|
+
|
|
90
|
+
- 历史教训: commit `9061cd0` 曾经实现"两遍扫描 + sanity helper"修复了正是这类问题, 但 commit `2bac9a7` revert 至 v0.3.0 时**连带回滚**了它 — revert 的真实目标是去除 `f497077` / `fdd4a92` / `43488a1` 引入的"zhipu 自清理通道"和"tool_result.id 注入"副作用, 两遍扫描属无辜方。**后续若再次需要 revert `vendor_channels.py`**, 必须先 `grep _enforce_pairing_sanity_pass` 与 `Pass 1` / `Pass 2` 注释, 确认这两段是核心修复而非可以一起回滚的实验性代码。
|
|
91
|
+
- 类似 "vendor 私有 ID 跨消息体改写" 场景 (如 doubao、minimax 未来若引入类似机制), 实现时同样应当遵循"先全局收集 id_map、后统一改写"的两阶段模式。
|
|
92
|
+
- 单元测试覆盖"块顺序敏感"类 bug 时, 建议在用例命名中显式标注顺序条件 (如 `test_two_pass_handles_inline_tool_result_before_server_tool_use`), 让未来 reviewer 一眼看出测试的边界价值。
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## count_tokens 路由 `AttributeError: 'ZhipuVendor' object has no attribute 'name'`
|
|
97
|
+
|
|
98
|
+
**问题描述**
|
|
99
|
+
|
|
100
|
+
后台日志反复出现 `POST /v1/messages/count_tokens?beta=true 500 Internal Server Error`,并伴随:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
File ".../coding/proxy/server/routes.py", line 153, in count_tokens
|
|
104
|
+
channel_fn = get_transition_channel(source, target_vendor.name)
|
|
105
|
+
AttributeError: 'ZhipuVendor' object has no attribute 'name'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
同一时间窗口内大量请求 200 OK、少量请求 500,呈"间歇性"故障特征。
|
|
109
|
+
|
|
110
|
+
**表因**
|
|
111
|
+
|
|
112
|
+
`src/coding/proxy/server/routes.py` 的 `count_tokens` 在 153 / 160 两处访问 `target_vendor.name`,触发 `AttributeError` 被 ASGI 中间件捕获返回 500。
|
|
113
|
+
|
|
114
|
+
**根因**
|
|
115
|
+
|
|
116
|
+
`BaseVendor` 仅暴露**抽象方法** `get_name() -> str`(`src/coding/proxy/vendors/base.py:75-77`),所有派生类(`AnthropicVendor`、`ZhipuVendor`、`CopilotVendor`、`MinimaxVendor`、`DoubaoVendor`、`KimiVendor` 等)均通过 `_vendor_name` 类属性配合 `get_name()` 返回名称 —— **并无 `name` 实例属性**。该错误访问在 lint/类型检查阶段无告警(因 `BaseVendor` 未在类型系统中约束 `name` 字段),仅在运行时触发。
|
|
117
|
+
|
|
118
|
+
间歇性原因:第 152 行 `if source:` 是守卫;`source` 由 `infer_source_vendor_from_body(body)`(`src/coding/proxy/convert/vendor_channels.py:357-394`)从请求体启发式推断,仅当出现 zhipu 私有产物(`srvtoolu_*` 形式的 `tool_use.id` 或 `server_tool_use` / `server_tool_use_delta` 类型 content block)时返回 `"zhipu"`,否则 `None`。纯净的首轮 count_tokens 请求 `source is None` 自然绕过 153 行,因此 200/500 共存。
|
|
119
|
+
|
|
120
|
+
**处理方式**
|
|
121
|
+
|
|
122
|
+
1. `routes.py:153,160` 将 `target_vendor.name` 改为 `target_vendor.get_name()`,并将结果提取到局部变量 `target_name` 复用,避免重复方法调用与日志/调用点不一致风险。
|
|
123
|
+
2. `tests/test_app_routes.py` 新增 `test_count_tokens_triggers_zhipu_to_target_channel`:通过注入 `server_tool_use` + `srvtoolu_*` 让 `infer_source_vendor_from_body` 返回 `"zhipu"`,断言返回 200 且 debug 日志含 `"count_tokens channel zhipu → anthropic"`,证明通道被实际触发。此前 6 个 count_tokens 测试的请求体都是纯净的、未触达该分支,是 bug 长期漏过的根因。
|
|
124
|
+
|
|
125
|
+
**后续防范**
|
|
126
|
+
|
|
127
|
+
- 跨模块引用 Vendor 实例字段时,**统一通过 `BaseVendor` 暴露的方法**(`get_name()`、`map_model()` 等),避免直接访问派生类未定义的"假属性"。
|
|
128
|
+
- 长期演进可考虑在 `BaseVendor` 增加 `@property name` 指向 `get_name()`,将契约前移到类型系统由 mypy / pyright 拦截 —— 该重构属"演进式设计"范畴,不在本次最小干预范围内。
|
|
129
|
+
- 测试覆盖原则:路由层涉及"内容感知"分支(如 `infer_source_vendor_from_body`)时,至少补一个让分支命中的最小用例,避免守卫掩盖代码缺陷。
|
|
130
|
+
|
|
131
|
+
**同类问题影响与处理注意事项**
|
|
132
|
+
|
|
133
|
+
- 已 `grep -rn "vendor\.name\b" src/` 全仓扫描,确认 `target_vendor.name | vendor.name` 误用仅 routes.py 的这两处,已随本次修复一并消除。`/v1/messages` 主链路在 executor 中调用 `tier.name`(`Tier` 对象的合法 dataclass 属性),与 vendor 实例 `name` 无关,不受影响。
|
|
134
|
+
- 若未来新增 Vendor 子类,仍只需实现 `get_name()` 抽象方法;外部调用方应遵循同一契约,本档案的修复模式可作为参考。
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
## 🌟 核心特性 (Core Features)
|
|
31
31
|
|
|
32
32
|
<div align="center">
|
|
33
|
-
<img src="../../assets/dashboard-v0.
|
|
33
|
+
<img src="../../assets/dashboard-v0.4.0.png">
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
- **⛓️ N-tier 链式故障转移 (Failover)**:自主降序序列,支持 Claude 官方 Plans,以及 GitHub Copilot、Google Antigravity、智谱、MiniMax、阿里千问、小米、Kimi、豆包等的 Coding Plan。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1a1"
|
|
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"
|
|
@@ -84,7 +84,10 @@ docstring-code-format = true
|
|
|
84
84
|
[tool.pytest.ini_options]
|
|
85
85
|
asyncio_mode = "auto"
|
|
86
86
|
testpaths = ["tests"]
|
|
87
|
-
addopts = "-v --tb=short"
|
|
87
|
+
addopts = "-v --tb=short -m 'not e2e'"
|
|
88
|
+
markers = [
|
|
89
|
+
"e2e: marks tests as end-to-end (deselect with '-m \"not e2e\"')",
|
|
90
|
+
]
|
|
88
91
|
filterwarnings = [
|
|
89
92
|
"ignore::DeprecationWarning",
|
|
90
93
|
]
|
|
@@ -219,9 +219,114 @@ def enforce_anthropic_tool_pairing(
|
|
|
219
219
|
", ".join(synthesized_ids),
|
|
220
220
|
)
|
|
221
221
|
|
|
222
|
+
# 纵深防御: sanity 兜底,捕获主循环未覆盖的边角配对漏洞
|
|
223
|
+
adaptations.extend(_enforce_pairing_sanity_pass(messages_list))
|
|
224
|
+
|
|
222
225
|
return adaptations
|
|
223
226
|
|
|
224
227
|
|
|
228
|
+
def _enforce_pairing_sanity_pass(
|
|
229
|
+
messages_list: list[dict[str, Any]],
|
|
230
|
+
) -> list[str]:
|
|
231
|
+
"""``enforce_anthropic_tool_pairing`` 主循环之后的纯检测兜底 helper.
|
|
232
|
+
|
|
233
|
+
职责正交于主循环(不剥离 tool_result、不插入新 user 消息),仅做两件事:
|
|
234
|
+
|
|
235
|
+
1. 遍历每个 ``role == "assistant"`` 且包含 ``tool_use`` 块的消息,
|
|
236
|
+
检查 ``messages[i+1]`` 是否为 ``user`` 且包含所有 ``tool_use.id`` 对应
|
|
237
|
+
``tool_result.tool_use_id``。
|
|
238
|
+
2. 缺失项在该 user 消息末尾追加 ``is_error=True`` 占位块;如果 next 消息根本
|
|
239
|
+
不是 user(主循环未触达此分支的退化场景),同样不做插入,仅记录 WARNING
|
|
240
|
+
供运维定位 —— 该路径正常情况下永不命中(主循环已保证 next user 存在)。
|
|
241
|
+
|
|
242
|
+
本 helper 单独抽出的目的有两个:
|
|
243
|
+
|
|
244
|
+
- 直接构造"绕过主循环"的输入做单元测试,确保 sanity 分支具备**正向回归保护**
|
|
245
|
+
(历史教训: ``9061cd0`` 引入两遍扫描+sanity 后被 ``2bac9a7`` 连带回滚,
|
|
246
|
+
重要原因之一是缺乏对兜底路径的独立单测)。
|
|
247
|
+
- 在主循环 A-F 步骤未来重构时,sanity 仍能稳定守住 Anthropic 配对约束。
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
messages_list: 消息列表(就地修改)。
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
新增的 adaptation 标签列表(命中则为 ``["pairing_sanity_repaired"]``,否则空列表)。
|
|
254
|
+
"""
|
|
255
|
+
repaired: list[tuple[int, str]] = []
|
|
256
|
+
|
|
257
|
+
for i, msg in enumerate(messages_list):
|
|
258
|
+
if not isinstance(msg, dict) or msg.get("role") != "assistant":
|
|
259
|
+
continue
|
|
260
|
+
content = msg.get("content")
|
|
261
|
+
if not isinstance(content, list):
|
|
262
|
+
continue
|
|
263
|
+
tool_use_ids = [
|
|
264
|
+
b["id"]
|
|
265
|
+
for b in content
|
|
266
|
+
if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("id")
|
|
267
|
+
]
|
|
268
|
+
if not tool_use_ids:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
next_idx = i + 1
|
|
272
|
+
if (
|
|
273
|
+
next_idx >= len(messages_list)
|
|
274
|
+
or not isinstance(messages_list[next_idx], dict)
|
|
275
|
+
or messages_list[next_idx].get("role") != "user"
|
|
276
|
+
):
|
|
277
|
+
# 主循环正常情况下已保证 next 为 user;此处仅日志告警,不做隐式插入
|
|
278
|
+
# 以避免与主循环职责重叠。
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Sanity pass: assistant at messages[%d] has tool_use without "
|
|
281
|
+
"user next message (tool_use_ids=%s). Main enforce loop may have a regression.",
|
|
282
|
+
i,
|
|
283
|
+
", ".join(tool_use_ids),
|
|
284
|
+
)
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
user_msg = messages_list[next_idx]
|
|
288
|
+
user_content = user_msg.get("content")
|
|
289
|
+
if not isinstance(user_content, list):
|
|
290
|
+
# 主循环 D 步已将 string content 归一化为 list;这里防御性兜底
|
|
291
|
+
user_msg["content"] = (
|
|
292
|
+
[{"type": "text", "text": user_content}]
|
|
293
|
+
if isinstance(user_content, str)
|
|
294
|
+
else []
|
|
295
|
+
)
|
|
296
|
+
user_content = user_msg["content"]
|
|
297
|
+
|
|
298
|
+
existing_result_ids = {
|
|
299
|
+
b["tool_use_id"]
|
|
300
|
+
for b in user_content
|
|
301
|
+
if isinstance(b, dict)
|
|
302
|
+
and b.get("type") == "tool_result"
|
|
303
|
+
and b.get("tool_use_id")
|
|
304
|
+
}
|
|
305
|
+
for uid in tool_use_ids:
|
|
306
|
+
if uid in existing_result_ids:
|
|
307
|
+
continue
|
|
308
|
+
user_content.append(
|
|
309
|
+
{
|
|
310
|
+
"type": "tool_result",
|
|
311
|
+
"tool_use_id": uid,
|
|
312
|
+
"content": "",
|
|
313
|
+
"is_error": True,
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
repaired.append((i, uid))
|
|
317
|
+
|
|
318
|
+
if not repaired:
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
logger.warning(
|
|
322
|
+
"Sanity pass repaired %d unpaired tool_use(s) missed by main enforce loop. "
|
|
323
|
+
"Affected: %s",
|
|
324
|
+
len(repaired),
|
|
325
|
+
", ".join(f"messages[{idx}]:{uid}" for idx, uid in repaired),
|
|
326
|
+
)
|
|
327
|
+
return ["pairing_sanity_repaired"]
|
|
328
|
+
|
|
329
|
+
|
|
225
330
|
def _strip_cache_control(body: dict[str, Any]) -> int:
|
|
226
331
|
"""从 system/messages/tools 中移除 cache_control 字段(就地).
|
|
227
332
|
|
|
@@ -294,8 +399,22 @@ def _rewrite_srvtoolu_ids(body: dict[str, Any]) -> tuple[int, dict[str, str]]:
|
|
|
294
399
|
|
|
295
400
|
Anthropic API 要求 tool_use 类型与 ``toolu_*`` 格式的 ID。Zhipu 的
|
|
296
401
|
``server_tool_use`` + ``srvtoolu_*`` 在上游 Anthropic 兼容端点可用,但无法
|
|
297
|
-
|
|
298
|
-
|
|
402
|
+
透传至其他供应商;同时还需重写所有 ``tool_result.tool_use_id`` 引用,保持配对关系。
|
|
403
|
+
|
|
404
|
+
**两遍扫描(消除块顺序敏感性)**:
|
|
405
|
+
|
|
406
|
+
- Pass 1: 仅遍历 ``role == "assistant"`` 的消息,按 assistant 出现顺序为每个
|
|
407
|
+
待改写的 tool_use 分配 ``toolu_normalized_N`` 新 ID,建立完整 ``id_map``。
|
|
408
|
+
- Pass 2: 全量遍历消息,对任意 ``tool_result.tool_use_id ∈ id_map`` 的块
|
|
409
|
+
原地改写为新 ID(不分 user / assistant,覆盖 misplaced 与跨消息边界场景)。
|
|
410
|
+
|
|
411
|
+
单遍方案在 GLM-5 偶发将 inline ``tool_result`` 输出在对应 ``server_tool_use``
|
|
412
|
+
之前的乱序场景下,会因 Case B 时 ``id_map`` 尚未填入而漏改 ``tool_use_id``,
|
|
413
|
+
导致 ``enforce_anthropic_tool_pairing`` 后 ``extracted_tool_results`` 的 key
|
|
414
|
+
与 ``tool_use_ids`` 不一致,进而把本应配对的 misplaced tool_result 默默丢弃,
|
|
415
|
+
最终触发 Anthropic ``messages.x: tool_use ids were found without tool_result
|
|
416
|
+
blocks immediately after`` 400 错误。两遍扫描以"先建表、后改写"的次序消除该
|
|
417
|
+
时序耦合。
|
|
299
418
|
|
|
300
419
|
Returns:
|
|
301
420
|
(rewritten_count, id_map) — 重写次数与 {原 ID: 新 ID} 映射。
|
|
@@ -308,45 +427,56 @@ def _rewrite_srvtoolu_ids(body: dict[str, Any]) -> tuple[int, dict[str, str]]:
|
|
|
308
427
|
counter += 1
|
|
309
428
|
return f"toolu_normalized_{counter}"
|
|
310
429
|
|
|
430
|
+
# Pass 1: 扫描 assistant 消息,改写 tool_use / server_tool_use 的 id 与 type,
|
|
431
|
+
# 按出现顺序填充 id_map(保持与单遍版本相同的序号分配,避免破坏既有断言)。
|
|
311
432
|
for message in body.get("messages", []):
|
|
312
|
-
if not isinstance(message, dict):
|
|
433
|
+
if not isinstance(message, dict) or message.get("role") != "assistant":
|
|
313
434
|
continue
|
|
314
435
|
content = message.get("content")
|
|
315
436
|
if not isinstance(content, list):
|
|
316
437
|
continue
|
|
317
|
-
role = message.get("role")
|
|
318
438
|
for block in content:
|
|
319
439
|
if not isinstance(block, dict):
|
|
320
440
|
continue
|
|
321
441
|
block_type = block.get("type")
|
|
442
|
+
if block_type not in {"tool_use", "server_tool_use"}:
|
|
443
|
+
continue
|
|
322
444
|
block_id = block.get("id")
|
|
323
|
-
|
|
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
|
-
|
|
445
|
+
if isinstance(block_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
|
|
446
|
+
block_id
|
|
447
|
+
):
|
|
448
|
+
new_id = next_id()
|
|
449
|
+
id_map[block_id] = new_id
|
|
450
|
+
block["id"] = new_id
|
|
451
|
+
block["type"] = "tool_use"
|
|
452
|
+
elif (
|
|
453
|
+
isinstance(block_id, str)
|
|
454
|
+
and block_id
|
|
455
|
+
and not _ANTHROPIC_TOOL_USE_ID_RE.match(block_id)
|
|
456
|
+
and block.get("name")
|
|
457
|
+
):
|
|
458
|
+
# 非标准 ID(非 toolu_ / srvtoolu_),且具备 name 可改写
|
|
459
|
+
new_id = next_id()
|
|
460
|
+
id_map[block_id] = new_id
|
|
461
|
+
block["id"] = new_id
|
|
462
|
+
block["type"] = "tool_use"
|
|
463
|
+
elif block_type == "server_tool_use" and isinstance(block_id, str):
|
|
464
|
+
# 兜底: 类型是 server_tool_use 但 ID 已是标准 toolu_ 形式,仅纠正类型
|
|
465
|
+
block["type"] = "tool_use"
|
|
466
|
+
|
|
467
|
+
# Pass 2: 全量扫描,对任意 tool_result.tool_use_id 命中 id_map 的块同步改写。
|
|
468
|
+
if id_map:
|
|
469
|
+
for message in body.get("messages", []):
|
|
470
|
+
if not isinstance(message, dict):
|
|
471
|
+
continue
|
|
472
|
+
content = message.get("content")
|
|
473
|
+
if not isinstance(content, list):
|
|
474
|
+
continue
|
|
475
|
+
for block in content:
|
|
476
|
+
if not isinstance(block, dict):
|
|
477
|
+
continue
|
|
478
|
+
if block.get("type") != "tool_result":
|
|
479
|
+
continue
|
|
350
480
|
tool_use_id = block.get("tool_use_id")
|
|
351
481
|
if isinstance(tool_use_id, str) and tool_use_id in id_map:
|
|
352
482
|
block["tool_use_id"] = id_map[tool_use_id]
|
|
@@ -18,6 +18,7 @@ import logging
|
|
|
18
18
|
import time
|
|
19
19
|
from collections.abc import AsyncIterator
|
|
20
20
|
from typing import TYPE_CHECKING
|
|
21
|
+
from urllib.parse import unquote
|
|
21
22
|
|
|
22
23
|
import httpx
|
|
23
24
|
|
|
@@ -172,8 +173,16 @@ class NativeProxyHandler:
|
|
|
172
173
|
)
|
|
173
174
|
|
|
174
175
|
method = request.method.upper()
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
# 防御性 URL 解码:确保 %3A → : 以兼容 Gemini :verb 路径语法。
|
|
177
|
+
# ASGI 规范要求 scope["path"] 已解码,但部分服务器/反向代理对
|
|
178
|
+
# 合法路径字符(如冒号)可能保留编码形态。
|
|
179
|
+
decoded_rest_path = unquote(rest_path)
|
|
180
|
+
operation = OperationClassifier.classify(provider, method, decoded_rest_path)
|
|
181
|
+
endpoint = (
|
|
182
|
+
decoded_rest_path
|
|
183
|
+
if decoded_rest_path.startswith("/")
|
|
184
|
+
else f"/{decoded_rest_path}"
|
|
185
|
+
)
|
|
177
186
|
|
|
178
187
|
upstream_headers = _filter_request_headers(dict(request.headers))
|
|
179
188
|
# 强制 identity —— 阻止上游压缩(httpx 默认会自动补 gzip,deflate;
|
|
@@ -51,27 +51,27 @@ _OPENAI_RULES: tuple[_Rule, ...] = (
|
|
|
51
51
|
# Gemini 的方法动词作为路径后缀(``:generateContent``),通过正则提取
|
|
52
52
|
_GEMINI_RULES: tuple[_Rule, ...] = (
|
|
53
53
|
_Rule(
|
|
54
|
-
re.compile(r"^/?v1(?:beta)?/models/[^/]
|
|
54
|
+
re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)streamGenerateContent/?$"),
|
|
55
55
|
"generate_content",
|
|
56
56
|
),
|
|
57
57
|
_Rule(
|
|
58
|
-
re.compile(r"^/?v1(?:beta)?/models/[^/]
|
|
58
|
+
re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)generateContent/?$"),
|
|
59
59
|
"generate_content",
|
|
60
60
|
),
|
|
61
61
|
_Rule(
|
|
62
|
-
re.compile(r"^/?v1(?:beta)?/models/[^/]
|
|
62
|
+
re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)countTokens/?$"),
|
|
63
63
|
"count_tokens",
|
|
64
64
|
),
|
|
65
65
|
_Rule(
|
|
66
|
-
re.compile(r"^/?v1(?:beta)?/models/[^/]
|
|
66
|
+
re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)embedContent/?$"),
|
|
67
67
|
"embedding",
|
|
68
68
|
),
|
|
69
69
|
_Rule(
|
|
70
|
-
re.compile(r"^/?v1(?:beta)?/models/[^/]
|
|
70
|
+
re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)batchEmbedContents/?$"),
|
|
71
71
|
"embedding.batch",
|
|
72
72
|
),
|
|
73
73
|
_Rule(
|
|
74
|
-
re.compile(r"^/?v1(?:beta)?/models/[^/]
|
|
74
|
+
re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)predict/?$"),
|
|
75
75
|
"predict",
|
|
76
76
|
),
|
|
77
77
|
_Rule(
|
|
@@ -159,7 +159,8 @@ class OperationClassifier:
|
|
|
159
159
|
normalized = path if path.startswith("/") else f"/{path}"
|
|
160
160
|
return bool(
|
|
161
161
|
re.match(
|
|
162
|
-
r"^/?v1(?:beta)?/models/[^/]
|
|
162
|
+
r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)streamGenerateContent/?$",
|
|
163
|
+
normalized,
|
|
163
164
|
)
|
|
164
165
|
)
|
|
165
166
|
|