coding-proxy 0.4.1a7__tar.gz → 0.4.1a9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/PKG-INFO +1 -1
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/agents/issue.md +44 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/pyproject.toml +1 -1
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/executor.py +182 -7
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_router_executor.py +375 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/uv.lock +1 -1
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/.gitignore +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/AGENTS.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/CHANGELOG.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/CLAUDE.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/LICENSE +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/README.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/agents/browser-validation.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/agents/knowledge-map.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/agents/reference-specifications.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/arch/convert.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/arch/routing.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/arch/testing.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/framework.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/ops/ci-cd.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/user-guide.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_banner.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_compat.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_config_init.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_copilot.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_currency.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_mixins.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_token.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_pricing.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_schema.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_tier.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_time_range.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_types.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_vendors.py +0 -0
- {coding_proxy-0.4.1a7 → coding_proxy-0.4.1a9}/tests/test_zhipu.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1a9
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
6
|
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
@@ -230,3 +230,47 @@ SUM(input_tokens + output_tokens
|
|
|
230
230
|
|
|
231
231
|
- 历次 PR 中 cache token 字段的引入是渐进式的(schema 已有四列、`log()` 入参齐全、Overview 已全口径消费),但部分聚合视图的口径升级被遗漏;任何向 `usage_log` 增列后,**必须**审计所有 `SUM(input_tokens` / `SUM(output_tokens` 出现处的聚合表达式是否需要同步更新。
|
|
232
232
|
- 跨标签页同一指标(如"总 Tokens")的口径一致性,建议在添加新视图时主动与 Overview 现有口径做交叉核对,必要时在 SQL 注释中标注口径来源,便于后续 review。
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Zhipu vendor 间歇性 `[1210][API 调用参数有误]` 拒绝(诊断阶段)
|
|
237
|
+
|
|
238
|
+
**问题描述**
|
|
239
|
+
|
|
240
|
+
Zhipu vendor 作为首选 tier 时,处理 `claude-haiku-* → glm-5-turbo` 的部分请求被上游直接拒绝:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
WARNING Tier zhipu semantic rejection
|
|
244
|
+
(type=invalid_request_error,
|
|
245
|
+
msg=[1210][API 调用参数有误,请检查文档。][...])
|
|
246
|
+
[model=claude-haiku-4-5-20251001, messages=1], trying next tier without recording failure
|
|
247
|
+
INFO Tier anthropic message succeeded (took over from failed tier: zhipu)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
失败请求统一表现为 `duration<1s + tokens=[0 0 0 0]`,被 zhipu 在入口校验阶段直接拒绝、未消耗任何 token。两次观察窗口失败率分别为 4%(2026-05-23 22:24,glm-4.7 旧映射)与 27%(2026-05-25 17:26+,glm-5-turbo 当前映射),均触发降级至 anthropic / copilot。
|
|
251
|
+
|
|
252
|
+
**表因**
|
|
253
|
+
|
|
254
|
+
`is_semantic_rejection` 检测到 zhipu 返回 `invalid_request_error + 1210` 含「API 调用参数有误」中文标记,判定为语义拒绝,跳过下一层 tier。1210 是智谱官方错误码,[官方文档](https://docs.bigmodel.cn/cn/api/api-code) 定义为「参数格式/类型不符规范」(区别于 1213「必需字段缺失」、1214「字段参数非法」)。
|
|
255
|
+
|
|
256
|
+
**根因(仍在收集证据)**
|
|
257
|
+
|
|
258
|
+
PR #244 的初版诊断字段仅覆盖 `thinking / thinking_blocks / cache_control / model / messages`,但 2026-05-25 17:26 后的诊断日志显示失败请求**均不含**上述任何字段。说明真正祸根在更细粒度的参数(system / tools / max_tokens / sampling / metadata / content_types / body_size 等)。
|
|
259
|
+
|
|
260
|
+
**处理方式(分阶段)**
|
|
261
|
+
|
|
262
|
+
- **Step 1(PR #244,已合并)**:在 `executor.py::_build_semantic_rejection_diagnostic` 中输出 thinking / cache_control 相关字段 — 但证据反转,覆盖不足以定位真因。
|
|
263
|
+
- **Step 1 v2(本次)**:扩展诊断函数覆盖 `system_kind|blocks(+cc)` / `tools` / `tool_choice` / 采样参数 / `stream` / `metadata_keys` / `content_types` / `body_bytes` 等维度。所有项「仅存在时输出」以控制日志噪声。配套 14 个单元测试(`TestBuildSemanticRejectionDiagnostic`)覆盖各字段组合。
|
|
264
|
+
- **Step 2(待定)**:依据扩展诊断日志的新证据,定位具体祸根参数后再施修复(候选路径:`ZhipuVendor._prepare_request` 参数剥离 / 调用现有 `normalize_for_zhipu` / pre-validation 警告)。
|
|
265
|
+
|
|
266
|
+
**后续防范**
|
|
267
|
+
|
|
268
|
+
- **「无证据,不下结论」**:当初版诊断字段无法覆盖根因时,禁止反复猜测,应优先扩展诊断维度抓取更多线索。本次先扩展再修复的迭代节奏可作为同类「黑盒 API 报错」问题的范式。
|
|
269
|
+
- **诊断字段设计原则**:所有诊断项应「仅存在时输出」,避免常态化噪声;输出格式紧凑(`key=val`)便于日志检索;参数值用 `!r:.N` 截断防止巨型对象灌入日志。
|
|
270
|
+
- **错误码差异化**:智谱 12xx 系列错误码语义并不等价(1210 ≠ 1213 ≠ 1214),未来面对类似 `[code][message]` 形式的供应商错误时,应优先查阅其官方错误码字典,避免基于错误消息字面意思的误判。
|
|
271
|
+
|
|
272
|
+
**同类问题影响与处理注意事项**
|
|
273
|
+
|
|
274
|
+
- 其他薄透传 vendor(minimax / kimi / doubao / alibaba / xiaomi)共用 `NativeAnthropicVendor._prepare_request`,若它们也开始报「参数错误」类语义拒绝,可复用本次扩展的诊断函数定位差异。
|
|
275
|
+
- 若证据指向 `tools` 字段(如工具 schema 不兼容)、`metadata` 字段(如自定义键被 zhipu 拒收)等具体路径,修复时应优先复用 `convert/vendor_channels.py` 中已有的 `normalize_for_zhipu` / `strip_thinking_blocks` 工具,避免在 vendor 内部重复实现剥离逻辑。
|
|
276
|
+
- 部署 Step 1 v2 后,建议观察至少 48 小时收集足够样本(>20 次失败),通过失败/成功请求形态对比统计找出**唯一差异维度**,再进入 Step 2。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.1a9"
|
|
4
4
|
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao..."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import logging
|
|
11
|
+
import re
|
|
10
12
|
import time
|
|
11
13
|
from collections.abc import AsyncIterator
|
|
12
14
|
from typing import Any
|
|
@@ -54,16 +56,71 @@ logger = logging.getLogger(__name__)
|
|
|
54
56
|
|
|
55
57
|
_SESSION_TITLE_MAX_LEN = 30
|
|
56
58
|
|
|
59
|
+
# Claude Code 注入的"噪声"标签 — 系统级上下文,不应进入 Session 标题。
|
|
60
|
+
# 这些标签由 CC harness 在首个 user 消息 content 中拼接,高度同质,
|
|
61
|
+
# 直接用作标题会导致跨会话标题无差异化,丧失辨识度。
|
|
62
|
+
_NOISE_TAG_PATTERN = re.compile(
|
|
63
|
+
r"<(?P<tag>system-reminder|user-preferences|"
|
|
64
|
+
r"local-command-stdout|local-command-stderr|"
|
|
65
|
+
r"bash-input|bash-stdout|bash-stderr|"
|
|
66
|
+
r"ide_selection|stdin|system_instruction)\b[^>]*>"
|
|
67
|
+
r".*?</(?P=tag)>",
|
|
68
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Slash command 子标签:用于识别 /commit、/review 等命令式调用,
|
|
72
|
+
# 合成"命令 + 参数"式标题。
|
|
73
|
+
_CMD_NAME_PATTERN = re.compile(r"<command-name>(.*?)</command-name>", flags=re.DOTALL)
|
|
74
|
+
_CMD_ARGS_PATTERN = re.compile(r"<command-args>(.*?)</command-args>", flags=re.DOTALL)
|
|
75
|
+
# 残留 command-* 包裹标签清除(command-message/command-stdout 等次要标签)。
|
|
76
|
+
_CMD_WRAPPER_PATTERN = re.compile(
|
|
77
|
+
r"<command-[\w-]+>.*?</command-[\w-]+>", flags=re.DOTALL
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _sanitize_user_text(raw: str) -> str:
|
|
82
|
+
"""剔除 Claude Code 注入的系统级 XML 块,还原真实用户输入。
|
|
83
|
+
|
|
84
|
+
处理顺序:
|
|
85
|
+
1. Slash command 优先识别 — 若检测到 <command-name>,合成"命令 + 参数"
|
|
86
|
+
式标题(因为残留文本通常为空,直接取标签内容更有意义)。
|
|
87
|
+
2. 通用噪声剥离 — 移除已知白名单内的 system-reminder 等标签。
|
|
88
|
+
3. 残留 command-* 包裹清除 — 兜底去除 command-message 等次要标签。
|
|
89
|
+
4. 前后空白归一化 — 折叠连续空白为单空格,便于 30 字截断。
|
|
90
|
+
"""
|
|
91
|
+
if not raw:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
# 阶段一: slash command 短路
|
|
95
|
+
cmd = _CMD_NAME_PATTERN.search(raw)
|
|
96
|
+
if cmd:
|
|
97
|
+
name = cmd.group(1).strip()
|
|
98
|
+
args_match = _CMD_ARGS_PATTERN.search(raw)
|
|
99
|
+
args = args_match.group(1).strip() if args_match else ""
|
|
100
|
+
composed = f"{name} {args}".strip() if args else name
|
|
101
|
+
if composed:
|
|
102
|
+
return composed
|
|
103
|
+
|
|
104
|
+
# 阶段二: 通用噪声剥离
|
|
105
|
+
cleaned = _NOISE_TAG_PATTERN.sub("", raw)
|
|
106
|
+
cleaned = _CMD_WRAPPER_PATTERN.sub("", cleaned)
|
|
107
|
+
|
|
108
|
+
# 阶段三: 空白折叠
|
|
109
|
+
return re.sub(r"\s+", " ", cleaned).strip()
|
|
110
|
+
|
|
57
111
|
|
|
58
112
|
def _extract_session_title(request: CanonicalRequest) -> str:
|
|
59
|
-
"""从规范化请求中提取首个用户消息文本作为 session
|
|
113
|
+
"""从规范化请求中提取首个用户消息文本作为 session 标题。
|
|
114
|
+
|
|
115
|
+
跳过 Claude Code 注入的系统级 XML 块(system-reminder、user-preferences 等),
|
|
116
|
+
确保标题反映用户真实输入而非高同质化的系统模板。
|
|
117
|
+
"""
|
|
60
118
|
for part in request.messages:
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return part.text.strip()[:_SESSION_TITLE_MAX_LEN]
|
|
119
|
+
if part.role != "user" or part.type != CanonicalPartType.TEXT:
|
|
120
|
+
continue
|
|
121
|
+
cleaned = _sanitize_user_text(part.text)
|
|
122
|
+
if cleaned:
|
|
123
|
+
return cleaned[:_SESSION_TITLE_MAX_LEN]
|
|
67
124
|
return ""
|
|
68
125
|
|
|
69
126
|
|
|
@@ -119,6 +176,124 @@ def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
|
|
|
119
176
|
return f" [{', '.join(parts)}]" if parts else ""
|
|
120
177
|
|
|
121
178
|
|
|
179
|
+
def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
|
|
180
|
+
"""构建语义拒绝的请求体诊断上下文.
|
|
181
|
+
|
|
182
|
+
在 semantic rejection 日志中附加请求体的可疑参数快照,
|
|
183
|
+
用于定位供应商参数校验失败的具体祸根参数。
|
|
184
|
+
|
|
185
|
+
覆盖范围:
|
|
186
|
+
* 模型 / messages 数(baseline)
|
|
187
|
+
* thinking 系列顶层参数 + history thinking_blocks 数
|
|
188
|
+
* system 形态(string / blocks,含 cache_control 计数)
|
|
189
|
+
* tools 数量 + tool_choice 形态
|
|
190
|
+
* 采样参数(max_tokens / temperature / top_p / top_k / stop_sequences)
|
|
191
|
+
* stream / metadata 形态
|
|
192
|
+
* cache_control 存在性
|
|
193
|
+
* messages.content 类型分布
|
|
194
|
+
* 请求体大小估算(json.dumps 字节数)
|
|
195
|
+
"""
|
|
196
|
+
parts: list[str] = []
|
|
197
|
+
|
|
198
|
+
# ── 模型 + 消息数(baseline,始终输出)──
|
|
199
|
+
parts.append(f"model={body.get('model', 'N/A')}")
|
|
200
|
+
parts.append(f"messages={len(body.get('messages', []))}")
|
|
201
|
+
|
|
202
|
+
# ── 顶层 thinking 系列参数 ──
|
|
203
|
+
for key in ("thinking", "extended_thinking", "reasoning_effort"):
|
|
204
|
+
if key in body:
|
|
205
|
+
val = body[key]
|
|
206
|
+
parts.append(f"{key}={val!r:.80}")
|
|
207
|
+
|
|
208
|
+
# ── system 形态 ──
|
|
209
|
+
system = body.get("system")
|
|
210
|
+
if isinstance(system, str):
|
|
211
|
+
parts.append(f"system_kind=string(len={len(system)})")
|
|
212
|
+
elif isinstance(system, list):
|
|
213
|
+
cc_count = sum(
|
|
214
|
+
1 for item in system if isinstance(item, dict) and "cache_control" in item
|
|
215
|
+
)
|
|
216
|
+
if cc_count:
|
|
217
|
+
parts.append(f"system_blocks={len(system)},cc={cc_count}")
|
|
218
|
+
else:
|
|
219
|
+
parts.append(f"system_blocks={len(system)}")
|
|
220
|
+
|
|
221
|
+
# ── tools 与 tool_choice ──
|
|
222
|
+
tools = body.get("tools")
|
|
223
|
+
if isinstance(tools, list):
|
|
224
|
+
parts.append(f"tools={len(tools)}")
|
|
225
|
+
tool_choice = body.get("tool_choice")
|
|
226
|
+
if tool_choice is not None:
|
|
227
|
+
parts.append(f"tool_choice={tool_choice!r:.60}")
|
|
228
|
+
|
|
229
|
+
# ── 采样参数(仅存在时输出)──
|
|
230
|
+
for key in ("max_tokens", "temperature", "top_p", "top_k"):
|
|
231
|
+
if key in body:
|
|
232
|
+
parts.append(f"{key}={body[key]!r:.40}")
|
|
233
|
+
stop_sequences = body.get("stop_sequences")
|
|
234
|
+
if isinstance(stop_sequences, list) and stop_sequences:
|
|
235
|
+
parts.append(f"stop_sequences={len(stop_sequences)}")
|
|
236
|
+
|
|
237
|
+
# ── stream / metadata ──
|
|
238
|
+
if "stream" in body:
|
|
239
|
+
parts.append(f"stream={body['stream']}")
|
|
240
|
+
metadata = body.get("metadata")
|
|
241
|
+
if isinstance(metadata, dict) and metadata:
|
|
242
|
+
parts.append(f"metadata_keys={len(metadata)}")
|
|
243
|
+
|
|
244
|
+
# ── 会话历史中的 thinking blocks 与 content_types 分布 ──
|
|
245
|
+
thinking_count = 0
|
|
246
|
+
content_type_counts: dict[str, int] = {}
|
|
247
|
+
for msg in body.get("messages", []):
|
|
248
|
+
content = msg.get("content")
|
|
249
|
+
if isinstance(content, str):
|
|
250
|
+
content_type_counts["string"] = content_type_counts.get("string", 0) + 1
|
|
251
|
+
continue
|
|
252
|
+
if not isinstance(content, list):
|
|
253
|
+
continue
|
|
254
|
+
for block in content:
|
|
255
|
+
if not isinstance(block, dict):
|
|
256
|
+
continue
|
|
257
|
+
btype = block.get("type")
|
|
258
|
+
if isinstance(btype, str):
|
|
259
|
+
content_type_counts[btype] = content_type_counts.get(btype, 0) + 1
|
|
260
|
+
if btype in ("thinking", "redacted_thinking"):
|
|
261
|
+
thinking_count += 1
|
|
262
|
+
if thinking_count:
|
|
263
|
+
parts.append(f"thinking_blocks_in_history={thinking_count}")
|
|
264
|
+
if content_type_counts:
|
|
265
|
+
type_repr = ",".join(f"{k}:{v}" for k, v in sorted(content_type_counts.items()))
|
|
266
|
+
parts.append(f"content_types={{{type_repr}}}")
|
|
267
|
+
|
|
268
|
+
# ── cache_control 存在检测(messages / tools,不含 system 因已单独统计)──
|
|
269
|
+
has_cc = False
|
|
270
|
+
sections: list[Any] = []
|
|
271
|
+
for m in body.get("messages", []):
|
|
272
|
+
if isinstance(m.get("content"), list):
|
|
273
|
+
sections.append(m["content"])
|
|
274
|
+
if isinstance(body.get("tools"), list):
|
|
275
|
+
sections.append(body["tools"])
|
|
276
|
+
for section in sections:
|
|
277
|
+
for item in section:
|
|
278
|
+
if isinstance(item, dict) and "cache_control" in item:
|
|
279
|
+
has_cc = True
|
|
280
|
+
break
|
|
281
|
+
if has_cc:
|
|
282
|
+
break
|
|
283
|
+
if has_cc:
|
|
284
|
+
parts.append("cache_control_fields=present")
|
|
285
|
+
|
|
286
|
+
# ── 请求体大小估算 ──
|
|
287
|
+
try:
|
|
288
|
+
body_bytes = len(json.dumps(body, ensure_ascii=False).encode("utf-8"))
|
|
289
|
+
parts.append(f"body_bytes={body_bytes}")
|
|
290
|
+
except (TypeError, ValueError):
|
|
291
|
+
# 极少数情况下 body 含非可序列化对象,跳过
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
return f" [{', '.join(parts)}]" if parts else ""
|
|
295
|
+
|
|
296
|
+
|
|
122
297
|
def _log_http_error_detail(
|
|
123
298
|
tier_name: str,
|
|
124
299
|
exc: Exception,
|
|
@@ -20,11 +20,15 @@ from coding.proxy.compat.canonical import (
|
|
|
20
20
|
build_canonical_request,
|
|
21
21
|
)
|
|
22
22
|
from coding.proxy.routing.executor import (
|
|
23
|
+
_SESSION_TITLE_MAX_LEN,
|
|
23
24
|
_VENDOR_PROTOCOL_LABEL_MAP,
|
|
25
|
+
_build_semantic_rejection_diagnostic,
|
|
26
|
+
_extract_session_title,
|
|
24
27
|
_has_tool_results,
|
|
25
28
|
_is_likely_request_format_error,
|
|
26
29
|
_log_vendor_response_error,
|
|
27
30
|
_RouteExecutor,
|
|
31
|
+
_sanitize_user_text,
|
|
28
32
|
)
|
|
29
33
|
from coding.proxy.routing.session_manager import RouteSessionManager
|
|
30
34
|
from coding.proxy.routing.tier import VendorTier
|
|
@@ -1949,3 +1953,374 @@ class TestPrepareBodyForTierTransition:
|
|
|
1949
1953
|
result = exec_inst._prepare_body_for_tier(body, tier, source_vendor="zhipu")
|
|
1950
1954
|
|
|
1951
1955
|
assert result is body
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
class TestBuildSemanticRejectionDiagnostic:
|
|
1959
|
+
"""覆盖 _build_semantic_rejection_diagnostic 函数 — 用于诊断 [1210] 等供应商语义拒绝.
|
|
1960
|
+
|
|
1961
|
+
重点验证:
|
|
1962
|
+
- baseline 字段(model / messages)始终输出
|
|
1963
|
+
- 仅当参数存在时才输出相关项(避免日志噪声)
|
|
1964
|
+
- 各字段输出格式稳定
|
|
1965
|
+
"""
|
|
1966
|
+
|
|
1967
|
+
def test_baseline_minimal_body(self):
|
|
1968
|
+
"""最小请求体:仅输出 model + messages."""
|
|
1969
|
+
body = {"model": "glm-5-turbo", "messages": [{"role": "user", "content": "hi"}]}
|
|
1970
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
1971
|
+
assert "model=glm-5-turbo" in result
|
|
1972
|
+
assert "messages=1" in result
|
|
1973
|
+
# 不应输出未使用的字段
|
|
1974
|
+
assert "thinking" not in result
|
|
1975
|
+
assert "tools" not in result
|
|
1976
|
+
assert "cache_control" not in result
|
|
1977
|
+
|
|
1978
|
+
def test_includes_thinking_param(self):
|
|
1979
|
+
body = {
|
|
1980
|
+
"model": "glm-5-turbo",
|
|
1981
|
+
"messages": [],
|
|
1982
|
+
"thinking": {"type": "enabled", "budget_tokens": 1024},
|
|
1983
|
+
}
|
|
1984
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
1985
|
+
assert "thinking=" in result
|
|
1986
|
+
assert "budget_tokens" in result
|
|
1987
|
+
|
|
1988
|
+
def test_includes_system_string(self):
|
|
1989
|
+
body = {
|
|
1990
|
+
"model": "glm-5-turbo",
|
|
1991
|
+
"messages": [],
|
|
1992
|
+
"system": "You are helpful." * 5,
|
|
1993
|
+
}
|
|
1994
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
1995
|
+
assert "system_kind=string(len=" in result
|
|
1996
|
+
|
|
1997
|
+
def test_includes_system_blocks_with_cache_control(self):
|
|
1998
|
+
body = {
|
|
1999
|
+
"model": "glm-5-turbo",
|
|
2000
|
+
"messages": [],
|
|
2001
|
+
"system": [
|
|
2002
|
+
{
|
|
2003
|
+
"type": "text",
|
|
2004
|
+
"text": "rule1",
|
|
2005
|
+
"cache_control": {"type": "ephemeral"},
|
|
2006
|
+
},
|
|
2007
|
+
{"type": "text", "text": "rule2"},
|
|
2008
|
+
],
|
|
2009
|
+
}
|
|
2010
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2011
|
+
assert "system_blocks=2,cc=1" in result
|
|
2012
|
+
|
|
2013
|
+
def test_includes_tools_and_tool_choice(self):
|
|
2014
|
+
body = {
|
|
2015
|
+
"model": "glm-5-turbo",
|
|
2016
|
+
"messages": [],
|
|
2017
|
+
"tools": [{"name": "a"}, {"name": "b"}, {"name": "c"}],
|
|
2018
|
+
"tool_choice": {"type": "auto"},
|
|
2019
|
+
}
|
|
2020
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2021
|
+
assert "tools=3" in result
|
|
2022
|
+
assert "tool_choice=" in result
|
|
2023
|
+
|
|
2024
|
+
def test_includes_sampling_params(self):
|
|
2025
|
+
body = {
|
|
2026
|
+
"model": "glm-5-turbo",
|
|
2027
|
+
"messages": [],
|
|
2028
|
+
"max_tokens": 8192,
|
|
2029
|
+
"temperature": 0.7,
|
|
2030
|
+
"top_p": 0.9,
|
|
2031
|
+
"top_k": 40,
|
|
2032
|
+
"stop_sequences": ["\n\n", "END"],
|
|
2033
|
+
}
|
|
2034
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2035
|
+
assert "max_tokens=8192" in result
|
|
2036
|
+
assert "temperature=0.7" in result
|
|
2037
|
+
assert "top_p=0.9" in result
|
|
2038
|
+
assert "top_k=40" in result
|
|
2039
|
+
assert "stop_sequences=2" in result
|
|
2040
|
+
|
|
2041
|
+
def test_includes_stream_and_metadata(self):
|
|
2042
|
+
body = {
|
|
2043
|
+
"model": "glm-5-turbo",
|
|
2044
|
+
"messages": [],
|
|
2045
|
+
"stream": True,
|
|
2046
|
+
"metadata": {"user_id": "x", "session_id": "y"},
|
|
2047
|
+
}
|
|
2048
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2049
|
+
assert "stream=True" in result
|
|
2050
|
+
assert "metadata_keys=2" in result
|
|
2051
|
+
|
|
2052
|
+
def test_content_type_distribution(self):
|
|
2053
|
+
body = {
|
|
2054
|
+
"model": "glm-5-turbo",
|
|
2055
|
+
"messages": [
|
|
2056
|
+
{
|
|
2057
|
+
"role": "user",
|
|
2058
|
+
"content": [
|
|
2059
|
+
{"type": "text", "text": "hi"},
|
|
2060
|
+
{"type": "text", "text": "bye"},
|
|
2061
|
+
{"type": "image", "source": {}},
|
|
2062
|
+
],
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
"role": "assistant",
|
|
2066
|
+
"content": [
|
|
2067
|
+
{"type": "tool_use", "id": "t1", "name": "x", "input": {}},
|
|
2068
|
+
],
|
|
2069
|
+
},
|
|
2070
|
+
],
|
|
2071
|
+
}
|
|
2072
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2073
|
+
# 排序为字母序
|
|
2074
|
+
assert "content_types={image:1,text:2,tool_use:1}" in result
|
|
2075
|
+
|
|
2076
|
+
def test_content_type_string_messages(self):
|
|
2077
|
+
"""messages.content 为 string 时计入 string:N."""
|
|
2078
|
+
body = {
|
|
2079
|
+
"model": "glm-5-turbo",
|
|
2080
|
+
"messages": [
|
|
2081
|
+
{"role": "user", "content": "hello"},
|
|
2082
|
+
{"role": "assistant", "content": "hi"},
|
|
2083
|
+
],
|
|
2084
|
+
}
|
|
2085
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2086
|
+
assert "content_types={string:2}" in result
|
|
2087
|
+
|
|
2088
|
+
def test_thinking_blocks_in_history(self):
|
|
2089
|
+
body = {
|
|
2090
|
+
"model": "glm-5-turbo",
|
|
2091
|
+
"messages": [
|
|
2092
|
+
{
|
|
2093
|
+
"role": "assistant",
|
|
2094
|
+
"content": [
|
|
2095
|
+
{"type": "thinking", "thinking": "..."},
|
|
2096
|
+
{"type": "redacted_thinking", "data": "..."},
|
|
2097
|
+
{"type": "text", "text": "result"},
|
|
2098
|
+
],
|
|
2099
|
+
}
|
|
2100
|
+
],
|
|
2101
|
+
}
|
|
2102
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2103
|
+
assert "thinking_blocks_in_history=2" in result
|
|
2104
|
+
|
|
2105
|
+
def test_cache_control_in_messages_or_tools(self):
|
|
2106
|
+
body = {
|
|
2107
|
+
"model": "glm-5-turbo",
|
|
2108
|
+
"messages": [
|
|
2109
|
+
{
|
|
2110
|
+
"role": "user",
|
|
2111
|
+
"content": [
|
|
2112
|
+
{
|
|
2113
|
+
"type": "text",
|
|
2114
|
+
"text": "x",
|
|
2115
|
+
"cache_control": {"type": "ephemeral"},
|
|
2116
|
+
},
|
|
2117
|
+
],
|
|
2118
|
+
}
|
|
2119
|
+
],
|
|
2120
|
+
}
|
|
2121
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2122
|
+
assert "cache_control_fields=present" in result
|
|
2123
|
+
|
|
2124
|
+
def test_body_bytes_estimated(self):
|
|
2125
|
+
body = {"model": "glm-5-turbo", "messages": [{"role": "user", "content": "ok"}]}
|
|
2126
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2127
|
+
assert "body_bytes=" in result
|
|
2128
|
+
|
|
2129
|
+
def test_body_bytes_skipped_when_unserializable(self):
|
|
2130
|
+
"""请求体含非可序列化对象时不抛异常."""
|
|
2131
|
+
|
|
2132
|
+
class NonSerializable:
|
|
2133
|
+
pass
|
|
2134
|
+
|
|
2135
|
+
body = {
|
|
2136
|
+
"model": "glm-5-turbo",
|
|
2137
|
+
"messages": [],
|
|
2138
|
+
"metadata": {"obj": NonSerializable()},
|
|
2139
|
+
}
|
|
2140
|
+
# 不应抛异常
|
|
2141
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2142
|
+
assert "model=glm-5-turbo" in result
|
|
2143
|
+
|
|
2144
|
+
def test_combined_real_world_failure_case(self):
|
|
2145
|
+
"""模拟真实失败请求形态(messages=1,无 thinking/cache_control,含 system + tools)."""
|
|
2146
|
+
body = {
|
|
2147
|
+
"model": "glm-5-turbo",
|
|
2148
|
+
"messages": [{"role": "user", "content": "需要修复一个 bug"}],
|
|
2149
|
+
"system": [{"type": "text", "text": "You are Claude Code."}],
|
|
2150
|
+
"tools": [{"name": "Read"}, {"name": "Edit"}],
|
|
2151
|
+
"max_tokens": 8192,
|
|
2152
|
+
"temperature": 1.0,
|
|
2153
|
+
"metadata": {"user_id": "x"},
|
|
2154
|
+
"stream": True,
|
|
2155
|
+
}
|
|
2156
|
+
result = _build_semantic_rejection_diagnostic(body)
|
|
2157
|
+
assert "model=glm-5-turbo" in result
|
|
2158
|
+
assert "messages=1" in result
|
|
2159
|
+
assert "system_blocks=1" in result
|
|
2160
|
+
assert "tools=2" in result
|
|
2161
|
+
assert "max_tokens=8192" in result
|
|
2162
|
+
assert "temperature=1.0" in result
|
|
2163
|
+
assert "metadata_keys=1" in result
|
|
2164
|
+
assert "stream=True" in result
|
|
2165
|
+
# 不应包含未出现的项
|
|
2166
|
+
assert "thinking_blocks_in_history" not in result
|
|
2167
|
+
assert "cache_control_fields" not in result
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
# ── Session 标题清洗与抽取测试 ─────────────────────────────────
|
|
2171
|
+
|
|
2172
|
+
|
|
2173
|
+
class TestSanitizeUserText:
|
|
2174
|
+
"""``_sanitize_user_text`` — 剥离 CC 注入的系统级 XML 块.
|
|
2175
|
+
|
|
2176
|
+
覆盖典型 system-reminder/user-preferences 噪声、slash command
|
|
2177
|
+
短路、空白折叠与边界场景。
|
|
2178
|
+
"""
|
|
2179
|
+
|
|
2180
|
+
def test_strips_system_reminder(self):
|
|
2181
|
+
raw = "<system-reminder>MCP 指令</system-reminder>这是用户真实输入"
|
|
2182
|
+
assert _sanitize_user_text(raw) == "这是用户真实输入"
|
|
2183
|
+
|
|
2184
|
+
def test_strips_user_preferences(self):
|
|
2185
|
+
raw = "用户问题<user-preferences>遵循 AGENTS.md</user-preferences>"
|
|
2186
|
+
assert _sanitize_user_text(raw) == "用户问题"
|
|
2187
|
+
|
|
2188
|
+
def test_strips_multiple_noise_blocks(self):
|
|
2189
|
+
raw = (
|
|
2190
|
+
"<system-reminder>A</system-reminder>"
|
|
2191
|
+
"<system-reminder>B</system-reminder>"
|
|
2192
|
+
"<system-reminder>C</system-reminder>"
|
|
2193
|
+
"<system-reminder>D</system-reminder>"
|
|
2194
|
+
"真实输入文本"
|
|
2195
|
+
"<user-preferences>P</user-preferences>"
|
|
2196
|
+
)
|
|
2197
|
+
assert _sanitize_user_text(raw) == "真实输入文本"
|
|
2198
|
+
|
|
2199
|
+
def test_strips_multiline_system_reminder(self):
|
|
2200
|
+
"""多行 system-reminder 块需被 DOTALL 完整匹配剥离."""
|
|
2201
|
+
raw = (
|
|
2202
|
+
"<system-reminder>\n"
|
|
2203
|
+
"# MCP Server Instructions\n"
|
|
2204
|
+
"Use this server to fetch ...\n"
|
|
2205
|
+
"</system-reminder>\n"
|
|
2206
|
+
"TITLE 中的 Session 标题应当取自用户输入"
|
|
2207
|
+
)
|
|
2208
|
+
assert _sanitize_user_text(raw) == "TITLE 中的 Session 标题应当取自用户输入"
|
|
2209
|
+
|
|
2210
|
+
def test_strips_tag_with_attributes(self):
|
|
2211
|
+
"""容忍标签携带属性(如 <system-reminder type="x">)."""
|
|
2212
|
+
raw = '<system-reminder type="x">noise</system-reminder>真实'
|
|
2213
|
+
assert _sanitize_user_text(raw) == "真实"
|
|
2214
|
+
|
|
2215
|
+
def test_slash_command_with_args(self):
|
|
2216
|
+
raw = (
|
|
2217
|
+
"<command-message>commit (user)</command-message>"
|
|
2218
|
+
"<command-name>/commit</command-name>"
|
|
2219
|
+
"<command-args>修复标题</command-args>"
|
|
2220
|
+
)
|
|
2221
|
+
assert _sanitize_user_text(raw) == "/commit 修复标题"
|
|
2222
|
+
|
|
2223
|
+
def test_slash_command_no_args(self):
|
|
2224
|
+
raw = "<command-name>/review</command-name>"
|
|
2225
|
+
assert _sanitize_user_text(raw) == "/review"
|
|
2226
|
+
|
|
2227
|
+
def test_collapses_whitespace(self):
|
|
2228
|
+
raw = "<system-reminder>X</system-reminder>\n\n 多余 空白\t\t折叠 "
|
|
2229
|
+
assert _sanitize_user_text(raw) == "多余 空白 折叠"
|
|
2230
|
+
|
|
2231
|
+
def test_empty_after_strip(self):
|
|
2232
|
+
raw = "<system-reminder>仅噪声</system-reminder>"
|
|
2233
|
+
assert _sanitize_user_text(raw) == ""
|
|
2234
|
+
|
|
2235
|
+
def test_empty_input(self):
|
|
2236
|
+
assert _sanitize_user_text("") == ""
|
|
2237
|
+
|
|
2238
|
+
def test_preserves_user_xml_like_content(self):
|
|
2239
|
+
"""用户输入中合法的 XML/HTML 片段(非白名单标签)需完整保留."""
|
|
2240
|
+
raw = "请帮我审查这段代码:<div>hello</div> 是否符合规范?"
|
|
2241
|
+
assert _sanitize_user_text(raw) == raw
|
|
2242
|
+
|
|
2243
|
+
def test_strips_local_command_output(self):
|
|
2244
|
+
raw = "<local-command-stdout>build ok</local-command-stdout>构建后的下一步问题"
|
|
2245
|
+
assert _sanitize_user_text(raw) == "构建后的下一步问题"
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
class TestExtractSessionTitle:
|
|
2249
|
+
"""``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题."""
|
|
2250
|
+
|
|
2251
|
+
@staticmethod
|
|
2252
|
+
def _build_request(messages: list[dict]):
|
|
2253
|
+
return build_canonical_request({"model": "test", "messages": messages}, {})
|
|
2254
|
+
|
|
2255
|
+
def test_truncates_to_max_len(self):
|
|
2256
|
+
long_text = "用户输入文本" * 20
|
|
2257
|
+
req = self._build_request([{"role": "user", "content": long_text}])
|
|
2258
|
+
title = _extract_session_title(req)
|
|
2259
|
+
assert len(title) == _SESSION_TITLE_MAX_LEN
|
|
2260
|
+
assert title == long_text[:_SESSION_TITLE_MAX_LEN]
|
|
2261
|
+
|
|
2262
|
+
def test_strips_noise_from_first_user_message(self):
|
|
2263
|
+
raw = (
|
|
2264
|
+
"<system-reminder>MCP 指令</system-reminder>"
|
|
2265
|
+
"<user-preferences>偏好</user-preferences>"
|
|
2266
|
+
"测试标题 ABC"
|
|
2267
|
+
)
|
|
2268
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2269
|
+
assert _extract_session_title(req) == "测试标题 ABC"
|
|
2270
|
+
|
|
2271
|
+
def test_handles_real_cc_first_message_shape(self):
|
|
2272
|
+
"""模拟 CC 真实首条消息(多个连续 system-reminder + 用户文本)."""
|
|
2273
|
+
raw = (
|
|
2274
|
+
"<system-reminder>\n# MCP Server Instructions\n...</system-reminder>"
|
|
2275
|
+
"<system-reminder>\nThe following skills...\n</system-reminder>"
|
|
2276
|
+
"<system-reminder>\nPlan mode is active...\n</system-reminder>"
|
|
2277
|
+
"\n\nTITLE 中的 Session 标题应当取自用户输入的信息前 30 个字\n\n"
|
|
2278
|
+
"<user-preferences>始终遵循 AGENTS.md</user-preferences>"
|
|
2279
|
+
)
|
|
2280
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2281
|
+
title = _extract_session_title(req)
|
|
2282
|
+
assert title.startswith("TITLE 中的 Session")
|
|
2283
|
+
assert len(title) <= _SESSION_TITLE_MAX_LEN
|
|
2284
|
+
|
|
2285
|
+
def test_extracts_slash_command(self):
|
|
2286
|
+
raw = (
|
|
2287
|
+
"<command-name>/commit</command-name>"
|
|
2288
|
+
"<command-args>feat: 新增标题清洗</command-args>"
|
|
2289
|
+
)
|
|
2290
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2291
|
+
assert _extract_session_title(req) == "/commit feat: 新增标题清洗"
|
|
2292
|
+
|
|
2293
|
+
def test_returns_empty_when_only_noise(self):
|
|
2294
|
+
raw = "<system-reminder>纯噪声</system-reminder>"
|
|
2295
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2296
|
+
assert _extract_session_title(req) == ""
|
|
2297
|
+
|
|
2298
|
+
def test_returns_empty_for_no_user_messages(self):
|
|
2299
|
+
req = self._build_request([{"role": "assistant", "content": "你好"}])
|
|
2300
|
+
assert _extract_session_title(req) == ""
|
|
2301
|
+
|
|
2302
|
+
def test_skips_noise_only_part_to_find_real_input(self):
|
|
2303
|
+
"""首个 user text part 全噪声时,fallback 到下一个非空 user part."""
|
|
2304
|
+
messages = [
|
|
2305
|
+
{
|
|
2306
|
+
"role": "user",
|
|
2307
|
+
"content": [
|
|
2308
|
+
{
|
|
2309
|
+
"type": "text",
|
|
2310
|
+
"text": "<system-reminder>noise</system-reminder>",
|
|
2311
|
+
},
|
|
2312
|
+
{"type": "text", "text": "真实问题"},
|
|
2313
|
+
],
|
|
2314
|
+
}
|
|
2315
|
+
]
|
|
2316
|
+
req = self._build_request(messages)
|
|
2317
|
+
assert _extract_session_title(req) == "真实问题"
|
|
2318
|
+
|
|
2319
|
+
def test_skips_assistant_role(self):
|
|
2320
|
+
"""assistant 角色的文本不应被作为标题候选."""
|
|
2321
|
+
messages = [
|
|
2322
|
+
{"role": "assistant", "content": "上一轮回答"},
|
|
2323
|
+
{"role": "user", "content": "新的用户问题"},
|
|
2324
|
+
]
|
|
2325
|
+
req = self._build_request(messages)
|
|
2326
|
+
assert _extract_session_title(req) == "新的用户问题"
|