coding-proxy 0.4.1a6__tar.gz → 0.4.1a8__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.1a6 → coding_proxy-0.4.1a8}/PKG-INFO +1 -1
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/pyproject.toml +1 -1
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/logging/db.py +38 -1
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/executor.py +90 -3
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/session_manager.py +9 -4
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/usage_recorder.py +5 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/server/dashboard.py +17 -11
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_router_executor.py +168 -5
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/uv.lock +1 -1
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/.gitignore +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/AGENTS.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/CHANGELOG.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/CLAUDE.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/LICENSE +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/README.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/agents/browser-validation.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/agents/issue.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/agents/knowledge-map.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/agents/reference-specifications.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/arch/convert.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/arch/routing.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/arch/testing.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/framework.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/ops/ci-cd.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/user-guide.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_banner.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_compat.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_config_init.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_copilot.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_currency.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_mixins.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_token.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_pricing.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_schema.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_tier.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_time_range.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_types.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/tests/test_vendors.py +0 -0
- {coding_proxy-0.4.1a6 → coding_proxy-0.4.1a8}/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.1a8
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.1a8"
|
|
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"
|
|
@@ -190,6 +190,14 @@ CREATE TABLE IF NOT EXISTS usage_evidence (
|
|
|
190
190
|
);
|
|
191
191
|
"""
|
|
192
192
|
|
|
193
|
+
_CREATE_SESSION_META = """
|
|
194
|
+
CREATE TABLE IF NOT EXISTS session_meta (
|
|
195
|
+
session_key TEXT PRIMARY KEY,
|
|
196
|
+
title TEXT NOT NULL DEFAULT '',
|
|
197
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
198
|
+
);
|
|
199
|
+
"""
|
|
200
|
+
|
|
193
201
|
_CREATE_INDEXES = """
|
|
194
202
|
CREATE INDEX IF NOT EXISTS idx_usage_ts ON usage_log(ts);
|
|
195
203
|
CREATE INDEX IF NOT EXISTS idx_usage_vendor ON usage_log(vendor);
|
|
@@ -245,6 +253,7 @@ class TokenLogger:
|
|
|
245
253
|
self._db.row_factory = aiosqlite.Row
|
|
246
254
|
await self._db.execute("PRAGMA journal_mode=WAL")
|
|
247
255
|
await self._db.executescript(_CREATE_TABLES)
|
|
256
|
+
await self._db.executescript(_CREATE_SESSION_META)
|
|
248
257
|
# 迁移必须在建索引之前执行,确保 vendor 列已存在
|
|
249
258
|
await self._migrate_rename_backend_to_vendor()
|
|
250
259
|
await self._migrate_add_failover_from()
|
|
@@ -316,6 +325,28 @@ class TokenLogger:
|
|
|
316
325
|
"Migration: renamed 'backend' column to 'vendor' in %s", table
|
|
317
326
|
)
|
|
318
327
|
|
|
328
|
+
async def set_session_title(self, session_key: str, title: str) -> None:
|
|
329
|
+
"""为新 session 设置标题(幂等,仅首次写入)."""
|
|
330
|
+
if not self._db or not title or not session_key:
|
|
331
|
+
return
|
|
332
|
+
await self._db.execute(
|
|
333
|
+
"INSERT OR IGNORE INTO session_meta (session_key, title) VALUES (?, ?)",
|
|
334
|
+
(session_key, title),
|
|
335
|
+
)
|
|
336
|
+
await self._db.commit()
|
|
337
|
+
|
|
338
|
+
async def get_session_titles(self, session_keys: list[str]) -> dict[str, str]:
|
|
339
|
+
"""批量查询 session 标题."""
|
|
340
|
+
if not self._db or not session_keys:
|
|
341
|
+
return {}
|
|
342
|
+
placeholders = ",".join("?" for _ in session_keys)
|
|
343
|
+
cursor = await self._db.execute(
|
|
344
|
+
f"SELECT session_key, title FROM session_meta WHERE session_key IN ({placeholders})",
|
|
345
|
+
session_keys,
|
|
346
|
+
)
|
|
347
|
+
rows = await cursor.fetchall()
|
|
348
|
+
return {row["session_key"]: row["title"] for row in rows}
|
|
349
|
+
|
|
319
350
|
async def log(
|
|
320
351
|
self,
|
|
321
352
|
vendor: str,
|
|
@@ -621,7 +652,13 @@ class TokenLogger:
|
|
|
621
652
|
(cutoff_iso, limit),
|
|
622
653
|
)
|
|
623
654
|
rows = await cursor.fetchall()
|
|
624
|
-
|
|
655
|
+
sessions = [dict(row) for row in rows]
|
|
656
|
+
if sessions:
|
|
657
|
+
keys = [s["session_key"] for s in sessions]
|
|
658
|
+
titles = await self.get_session_titles(keys)
|
|
659
|
+
for s in sessions:
|
|
660
|
+
s["title"] = titles.get(s["session_key"], "")
|
|
661
|
+
return sessions
|
|
625
662
|
|
|
626
663
|
async def query_session_profile(self, session_key: str) -> dict | None:
|
|
627
664
|
"""查询单个会话的完整聚合数据."""
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
+
import re
|
|
10
11
|
import time
|
|
11
12
|
from collections.abc import AsyncIterator
|
|
12
13
|
from typing import Any
|
|
@@ -43,10 +44,84 @@ from .usage_recorder import UsageRecorder
|
|
|
43
44
|
# 向后兼容别名
|
|
44
45
|
BackendResponse = VendorResponse
|
|
45
46
|
NoCompatibleBackendError = NoCompatibleVendorError
|
|
46
|
-
from ..compat.canonical import
|
|
47
|
+
from ..compat.canonical import (
|
|
48
|
+
CanonicalPartType,
|
|
49
|
+
CompatibilityStatus,
|
|
50
|
+
build_canonical_request,
|
|
51
|
+
)
|
|
52
|
+
from ..model.compat import CanonicalRequest
|
|
47
53
|
|
|
48
54
|
logger = logging.getLogger(__name__)
|
|
49
55
|
|
|
56
|
+
_SESSION_TITLE_MAX_LEN = 30
|
|
57
|
+
|
|
58
|
+
# Claude Code 注入的"噪声"标签 — 系统级上下文,不应进入 Session 标题。
|
|
59
|
+
# 这些标签由 CC harness 在首个 user 消息 content 中拼接,高度同质,
|
|
60
|
+
# 直接用作标题会导致跨会话标题无差异化,丧失辨识度。
|
|
61
|
+
_NOISE_TAG_PATTERN = re.compile(
|
|
62
|
+
r"<(?P<tag>system-reminder|user-preferences|"
|
|
63
|
+
r"local-command-stdout|local-command-stderr|"
|
|
64
|
+
r"bash-input|bash-stdout|bash-stderr|"
|
|
65
|
+
r"ide_selection|stdin|system_instruction)\b[^>]*>"
|
|
66
|
+
r".*?</(?P=tag)>",
|
|
67
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Slash command 子标签:用于识别 /commit、/review 等命令式调用,
|
|
71
|
+
# 合成"命令 + 参数"式标题。
|
|
72
|
+
_CMD_NAME_PATTERN = re.compile(r"<command-name>(.*?)</command-name>", flags=re.DOTALL)
|
|
73
|
+
_CMD_ARGS_PATTERN = re.compile(r"<command-args>(.*?)</command-args>", flags=re.DOTALL)
|
|
74
|
+
# 残留 command-* 包裹标签清除(command-message/command-stdout 等次要标签)。
|
|
75
|
+
_CMD_WRAPPER_PATTERN = re.compile(
|
|
76
|
+
r"<command-[\w-]+>.*?</command-[\w-]+>", flags=re.DOTALL
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _sanitize_user_text(raw: str) -> str:
|
|
81
|
+
"""剔除 Claude Code 注入的系统级 XML 块,还原真实用户输入。
|
|
82
|
+
|
|
83
|
+
处理顺序:
|
|
84
|
+
1. Slash command 优先识别 — 若检测到 <command-name>,合成"命令 + 参数"
|
|
85
|
+
式标题(因为残留文本通常为空,直接取标签内容更有意义)。
|
|
86
|
+
2. 通用噪声剥离 — 移除已知白名单内的 system-reminder 等标签。
|
|
87
|
+
3. 残留 command-* 包裹清除 — 兜底去除 command-message 等次要标签。
|
|
88
|
+
4. 前后空白归一化 — 折叠连续空白为单空格,便于 30 字截断。
|
|
89
|
+
"""
|
|
90
|
+
if not raw:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
# 阶段一: slash command 短路
|
|
94
|
+
cmd = _CMD_NAME_PATTERN.search(raw)
|
|
95
|
+
if cmd:
|
|
96
|
+
name = cmd.group(1).strip()
|
|
97
|
+
args_match = _CMD_ARGS_PATTERN.search(raw)
|
|
98
|
+
args = args_match.group(1).strip() if args_match else ""
|
|
99
|
+
composed = f"{name} {args}".strip() if args else name
|
|
100
|
+
if composed:
|
|
101
|
+
return composed
|
|
102
|
+
|
|
103
|
+
# 阶段二: 通用噪声剥离
|
|
104
|
+
cleaned = _NOISE_TAG_PATTERN.sub("", raw)
|
|
105
|
+
cleaned = _CMD_WRAPPER_PATTERN.sub("", cleaned)
|
|
106
|
+
|
|
107
|
+
# 阶段三: 空白折叠
|
|
108
|
+
return re.sub(r"\s+", " ", cleaned).strip()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_session_title(request: CanonicalRequest) -> str:
|
|
112
|
+
"""从规范化请求中提取首个用户消息文本作为 session 标题。
|
|
113
|
+
|
|
114
|
+
跳过 Claude Code 注入的系统级 XML 块(system-reminder、user-preferences 等),
|
|
115
|
+
确保标题反映用户真实输入而非高同质化的系统模板。
|
|
116
|
+
"""
|
|
117
|
+
for part in request.messages:
|
|
118
|
+
if part.role != "user" or part.type != CanonicalPartType.TEXT:
|
|
119
|
+
continue
|
|
120
|
+
cleaned = _sanitize_user_text(part.text)
|
|
121
|
+
if cleaned:
|
|
122
|
+
return cleaned[:_SESSION_TITLE_MAX_LEN]
|
|
123
|
+
return ""
|
|
124
|
+
|
|
50
125
|
|
|
51
126
|
def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
|
|
52
127
|
"""构建语义拒绝的请求体诊断上下文.
|
|
@@ -393,10 +468,16 @@ class _RouteExecutor:
|
|
|
393
468
|
failed_tier_name: str | None = None
|
|
394
469
|
request_caps = build_request_capabilities(body)
|
|
395
470
|
canonical_request = build_canonical_request(body, headers)
|
|
396
|
-
session_record = await self._session_mgr.get_or_create_record(
|
|
471
|
+
session_record, is_new_session = await self._session_mgr.get_or_create_record(
|
|
397
472
|
canonical_request.session_key,
|
|
398
473
|
canonical_request.trace_id,
|
|
399
474
|
)
|
|
475
|
+
if is_new_session:
|
|
476
|
+
title = _extract_session_title(canonical_request)
|
|
477
|
+
if title:
|
|
478
|
+
await self._recorder.set_session_title(
|
|
479
|
+
canonical_request.session_key, title
|
|
480
|
+
)
|
|
400
481
|
incompatible_reasons: list[str] = []
|
|
401
482
|
effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
|
|
402
483
|
last_idx = len(effective_tiers) - 1
|
|
@@ -564,10 +645,16 @@ class _RouteExecutor:
|
|
|
564
645
|
failed_tier_name: str | None = None
|
|
565
646
|
request_caps = build_request_capabilities(body)
|
|
566
647
|
canonical_request = build_canonical_request(body, headers)
|
|
567
|
-
session_record = await self._session_mgr.get_or_create_record(
|
|
648
|
+
session_record, is_new_session = await self._session_mgr.get_or_create_record(
|
|
568
649
|
canonical_request.session_key,
|
|
569
650
|
canonical_request.trace_id,
|
|
570
651
|
)
|
|
652
|
+
if is_new_session:
|
|
653
|
+
title = _extract_session_title(canonical_request)
|
|
654
|
+
if title:
|
|
655
|
+
await self._recorder.set_session_title(
|
|
656
|
+
canonical_request.session_key, title
|
|
657
|
+
)
|
|
571
658
|
incompatible_reasons: list[str] = []
|
|
572
659
|
effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
|
|
573
660
|
last_idx = len(effective_tiers) - 1
|
|
@@ -19,13 +19,18 @@ class RouteSessionManager:
|
|
|
19
19
|
|
|
20
20
|
async def get_or_create_record(
|
|
21
21
|
self, session_key: str, trace_id: str
|
|
22
|
-
) -> CompatSessionRecord | None:
|
|
22
|
+
) -> tuple[CompatSessionRecord | None, bool]:
|
|
23
|
+
"""获取或创建兼容性会话记录.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
(record, is_new) — is_new 为 True 表示本次创建的新会话。
|
|
27
|
+
"""
|
|
23
28
|
if self._store is None:
|
|
24
|
-
return None
|
|
29
|
+
return None, False
|
|
25
30
|
record = await self._store.get(session_key)
|
|
26
31
|
if record is not None:
|
|
27
|
-
return record
|
|
28
|
-
return CompatSessionRecord(session_key=session_key, trace_id=trace_id)
|
|
32
|
+
return record, False
|
|
33
|
+
return CompatSessionRecord(session_key=session_key, trace_id=trace_id), True
|
|
29
34
|
|
|
30
35
|
def apply_compat_context(
|
|
31
36
|
self,
|
|
@@ -28,6 +28,11 @@ class UsageRecorder:
|
|
|
28
28
|
def set_pricing_table(self, table: PricingTable) -> None:
|
|
29
29
|
self._pricing_table = table
|
|
30
30
|
|
|
31
|
+
async def set_session_title(self, session_key: str, title: str) -> None:
|
|
32
|
+
"""为新 session 设置标题(委托给 TokenLogger)."""
|
|
33
|
+
if self._token_logger:
|
|
34
|
+
await self._token_logger.set_session_title(session_key, title)
|
|
35
|
+
|
|
31
36
|
# ── 用量信息构建 ──────────────────────────────────────
|
|
32
37
|
|
|
33
38
|
@staticmethod
|
|
@@ -411,6 +411,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
411
411
|
.session-table td.cell-tags { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.8; vertical-align: middle; }
|
|
412
412
|
.session-table tr:hover td { background: var(--bg-card-hover); }
|
|
413
413
|
.session-table .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
414
|
+
.session-table .session-title { font-size: 12px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
|
|
414
415
|
.session-id { display: flex; align-items: center; gap: 4px; }
|
|
415
416
|
.session-id-text { overflow: hidden; text-overflow: ellipsis; }
|
|
416
417
|
.copy-btn { background: none; border: none; color: var(--text-tertiary); cursor: pointer; padding: 2px; border-radius: 4px; font-size: 12px; line-height: 1; opacity: .5; flex-shrink: 0; }
|
|
@@ -676,20 +677,22 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
676
677
|
<div class="session-table-wrap" id="sessions-table-wrap">
|
|
677
678
|
<table class="session-table">
|
|
678
679
|
<colgroup>
|
|
679
|
-
<col style="width:
|
|
680
|
-
<col style="width:
|
|
680
|
+
<col style="width:10%">
|
|
681
|
+
<col style="width:15%">
|
|
681
682
|
<col style="width:6%">
|
|
683
|
+
<col style="width:5%">
|
|
684
|
+
<col style="width:5%">
|
|
685
|
+
<col style="width:15%">
|
|
686
|
+
<col style="width:10%">
|
|
682
687
|
<col style="width:6%">
|
|
683
|
-
<col style="width:
|
|
684
|
-
<col style="width:
|
|
685
|
-
<col style="width:
|
|
686
|
-
<col style="width:9%">
|
|
687
|
-
<col style="width:12%">
|
|
688
|
-
<col style="width:12%">
|
|
688
|
+
<col style="width:8%">
|
|
689
|
+
<col style="width:10%">
|
|
690
|
+
<col style="width:10%">
|
|
689
691
|
</colgroup>
|
|
690
692
|
<thead>
|
|
691
693
|
<tr>
|
|
692
694
|
<th>Session ID</th>
|
|
695
|
+
<th>Title</th>
|
|
693
696
|
<th>Last Active</th>
|
|
694
697
|
<th>Requests</th>
|
|
695
698
|
<th>Tokens</th>
|
|
@@ -702,7 +705,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
702
705
|
</tr>
|
|
703
706
|
</thead>
|
|
704
707
|
<tbody id="sessions-tbody">
|
|
705
|
-
<tr><td colspan="
|
|
708
|
+
<tr><td colspan="11" class="empty">Loading...</td></tr>
|
|
706
709
|
</tbody>
|
|
707
710
|
</table>
|
|
708
711
|
<div class="session-pagination" id="session-pagination">
|
|
@@ -1573,7 +1576,7 @@ function renderSessionPage() {
|
|
|
1573
1576
|
var tbody = document.getElementById('sessions-tbody');
|
|
1574
1577
|
|
|
1575
1578
|
if (!total) {
|
|
1576
|
-
tbody.innerHTML = '<tr><td colspan="
|
|
1579
|
+
tbody.innerHTML = '<tr><td colspan="11" class="empty"><div class="empty-icon">📭</div>No session data</td></tr>';
|
|
1577
1580
|
} else {
|
|
1578
1581
|
tbody.innerHTML = page.map(function(s) {
|
|
1579
1582
|
var parsed = parseSessionKey(s.session_key);
|
|
@@ -1582,6 +1585,7 @@ function renderSessionPage() {
|
|
|
1582
1585
|
var modelsFull = (s.models || '').split(',').map(function(c){return c.trim();});
|
|
1583
1586
|
var vendorsFull = (s.vendors || '').split(',').map(function(v){return formatVendorLabel(v.trim());});
|
|
1584
1587
|
var sr = s.success_rate != null ? Math.round(s.success_rate) : null;
|
|
1588
|
+
var sessionTitle = s.title || '';
|
|
1585
1589
|
return '<tr data-row onclick="toggleRow(this)">' +
|
|
1586
1590
|
'<td class="session-key" onclick="event.stopPropagation()">' +
|
|
1587
1591
|
'<div class="session-id" data-key="' + escapeHtml(s.session_key) + '" title="' + escapeHtml(s.session_key) + '">' +
|
|
@@ -1592,6 +1596,7 @@ function renderSessionPage() {
|
|
|
1592
1596
|
'dev:' + escapeHtml(shortId(parsed.device_id, 8)) + ' · acct:' + escapeHtml(shortId(parsed.account_uuid, 8)) +
|
|
1593
1597
|
'</div>' +
|
|
1594
1598
|
'</td>' +
|
|
1599
|
+
'<td class="session-title" title="' + escapeHtml(sessionTitle) + '">' + (sessionTitle ? escapeHtml(sessionTitle) : '–') + '</td>' +
|
|
1595
1600
|
'<td>' + relativeTime(s.last_active_ts) + '</td>' +
|
|
1596
1601
|
'<td style="font-family:JetBrains Mono,monospace">' + fmtNum(s.total_requests) + '</td>' +
|
|
1597
1602
|
'<td style="font-family:JetBrains Mono,monospace">' + fmtTokens(s.total_tokens) + '</td>' +
|
|
@@ -1602,9 +1607,10 @@ function renderSessionPage() {
|
|
|
1602
1607
|
'<td onclick="event.stopPropagation()">' + selectHtml + '</td>' +
|
|
1603
1608
|
'<td>' + formatCategories(s.client_categories) + '</td>' +
|
|
1604
1609
|
'</tr>' +
|
|
1605
|
-
'<tr class="row-detail"><td colspan="
|
|
1610
|
+
'<tr class="row-detail"><td colspan="11"><div class="detail-card">' +
|
|
1606
1611
|
'<div class="detail-identity-row">' +
|
|
1607
1612
|
'<div class="detail-item"><div class="detail-label">Session ID</div><div class="detail-value" title="' + escapeHtml(s.session_key) + '">' + escapeHtml(parsed.session_id || s.session_key) + '</div></div>' +
|
|
1613
|
+
'<div class="detail-item"><div class="detail-label">Title</div><div class="detail-value">' + (sessionTitle ? escapeHtml(sessionTitle) : '–') + '</div></div>' +
|
|
1608
1614
|
'<div class="detail-item"><div class="detail-label">Device</div><div class="detail-value" title="' + escapeHtml(parsed.device_id || '') + '">' + (parsed.device_id ? escapeHtml(parsed.device_id) : '–') + '</div></div>' +
|
|
1609
1615
|
'<div class="detail-item"><div class="detail-label">Account</div><div class="detail-value" title="' + escapeHtml(parsed.account_uuid || '') + '">' + (parsed.account_uuid ? escapeHtml(parsed.account_uuid) : '–') + '</div></div>' +
|
|
1610
1616
|
'</div>' +
|
|
@@ -20,11 +20,14 @@ 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
|
+
_extract_session_title,
|
|
24
26
|
_has_tool_results,
|
|
25
27
|
_is_likely_request_format_error,
|
|
26
28
|
_log_vendor_response_error,
|
|
27
29
|
_RouteExecutor,
|
|
30
|
+
_sanitize_user_text,
|
|
28
31
|
)
|
|
29
32
|
from coding.proxy.routing.session_manager import RouteSessionManager
|
|
30
33
|
from coding.proxy.routing.tier import VendorTier
|
|
@@ -222,7 +225,7 @@ class TestTryGateTier:
|
|
|
222
225
|
headers = {}
|
|
223
226
|
caps = RequestCapabilities()
|
|
224
227
|
req = build_canonical_request(body, headers)
|
|
225
|
-
session_record = await exec_inst._session_mgr.get_or_create_record(
|
|
228
|
+
session_record, _is_new = await exec_inst._session_mgr.get_or_create_record(
|
|
226
229
|
req.session_key, req.trace_id
|
|
227
230
|
)
|
|
228
231
|
reasons: list[str] = []
|
|
@@ -246,7 +249,7 @@ class TestTryGateTier:
|
|
|
246
249
|
body = {"model": "test"}
|
|
247
250
|
headers = {}
|
|
248
251
|
req = build_canonical_request(body, headers)
|
|
249
|
-
session_record = await exec_inst._session_mgr.get_or_create_record(
|
|
252
|
+
session_record, _is_new = await exec_inst._session_mgr.get_or_create_record(
|
|
250
253
|
req.session_key, req.trace_id
|
|
251
254
|
)
|
|
252
255
|
reasons: list[str] = []
|
|
@@ -275,7 +278,7 @@ class TestTryGateTier:
|
|
|
275
278
|
body = {"model": "test", "thinking": {"type": "enabled"}}
|
|
276
279
|
headers = {}
|
|
277
280
|
req = build_canonical_request(body, headers)
|
|
278
|
-
session_record = await exec_inst._session_mgr.get_or_create_record(
|
|
281
|
+
session_record, _is_new = await exec_inst._session_mgr.get_or_create_record(
|
|
279
282
|
req.session_key, req.trace_id
|
|
280
283
|
)
|
|
281
284
|
reasons: list[str] = []
|
|
@@ -651,9 +654,10 @@ class TestRouteSessionManagerIntegration:
|
|
|
651
654
|
@pytest.mark.asyncio
|
|
652
655
|
async def test_get_or_create_without_store(self):
|
|
653
656
|
mgr = RouteSessionManager(compat_session_store=None)
|
|
654
|
-
record = await mgr.get_or_create_record("sk_test", "trace_1")
|
|
655
|
-
# 无 store 时返回 None
|
|
657
|
+
record, is_new = await mgr.get_or_create_record("sk_test", "trace_1")
|
|
658
|
+
# 无 store 时返回 (None, False)
|
|
656
659
|
assert record is None
|
|
660
|
+
assert is_new is False
|
|
657
661
|
|
|
658
662
|
@pytest.mark.asyncio
|
|
659
663
|
async def test_persist_session_without_store_is_noop(self):
|
|
@@ -1948,3 +1952,162 @@ class TestPrepareBodyForTierTransition:
|
|
|
1948
1952
|
result = exec_inst._prepare_body_for_tier(body, tier, source_vendor="zhipu")
|
|
1949
1953
|
|
|
1950
1954
|
assert result is body
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
# ── Session 标题清洗与抽取测试 ─────────────────────────────────
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
class TestSanitizeUserText:
|
|
1961
|
+
"""``_sanitize_user_text`` — 剥离 CC 注入的系统级 XML 块.
|
|
1962
|
+
|
|
1963
|
+
覆盖典型 system-reminder/user-preferences 噪声、slash command
|
|
1964
|
+
短路、空白折叠与边界场景。
|
|
1965
|
+
"""
|
|
1966
|
+
|
|
1967
|
+
def test_strips_system_reminder(self):
|
|
1968
|
+
raw = "<system-reminder>MCP 指令</system-reminder>这是用户真实输入"
|
|
1969
|
+
assert _sanitize_user_text(raw) == "这是用户真实输入"
|
|
1970
|
+
|
|
1971
|
+
def test_strips_user_preferences(self):
|
|
1972
|
+
raw = "用户问题<user-preferences>遵循 AGENTS.md</user-preferences>"
|
|
1973
|
+
assert _sanitize_user_text(raw) == "用户问题"
|
|
1974
|
+
|
|
1975
|
+
def test_strips_multiple_noise_blocks(self):
|
|
1976
|
+
raw = (
|
|
1977
|
+
"<system-reminder>A</system-reminder>"
|
|
1978
|
+
"<system-reminder>B</system-reminder>"
|
|
1979
|
+
"<system-reminder>C</system-reminder>"
|
|
1980
|
+
"<system-reminder>D</system-reminder>"
|
|
1981
|
+
"真实输入文本"
|
|
1982
|
+
"<user-preferences>P</user-preferences>"
|
|
1983
|
+
)
|
|
1984
|
+
assert _sanitize_user_text(raw) == "真实输入文本"
|
|
1985
|
+
|
|
1986
|
+
def test_strips_multiline_system_reminder(self):
|
|
1987
|
+
"""多行 system-reminder 块需被 DOTALL 完整匹配剥离."""
|
|
1988
|
+
raw = (
|
|
1989
|
+
"<system-reminder>\n"
|
|
1990
|
+
"# MCP Server Instructions\n"
|
|
1991
|
+
"Use this server to fetch ...\n"
|
|
1992
|
+
"</system-reminder>\n"
|
|
1993
|
+
"TITLE 中的 Session 标题应当取自用户输入"
|
|
1994
|
+
)
|
|
1995
|
+
assert _sanitize_user_text(raw) == "TITLE 中的 Session 标题应当取自用户输入"
|
|
1996
|
+
|
|
1997
|
+
def test_strips_tag_with_attributes(self):
|
|
1998
|
+
"""容忍标签携带属性(如 <system-reminder type="x">)."""
|
|
1999
|
+
raw = '<system-reminder type="x">noise</system-reminder>真实'
|
|
2000
|
+
assert _sanitize_user_text(raw) == "真实"
|
|
2001
|
+
|
|
2002
|
+
def test_slash_command_with_args(self):
|
|
2003
|
+
raw = (
|
|
2004
|
+
"<command-message>commit (user)</command-message>"
|
|
2005
|
+
"<command-name>/commit</command-name>"
|
|
2006
|
+
"<command-args>修复标题</command-args>"
|
|
2007
|
+
)
|
|
2008
|
+
assert _sanitize_user_text(raw) == "/commit 修复标题"
|
|
2009
|
+
|
|
2010
|
+
def test_slash_command_no_args(self):
|
|
2011
|
+
raw = "<command-name>/review</command-name>"
|
|
2012
|
+
assert _sanitize_user_text(raw) == "/review"
|
|
2013
|
+
|
|
2014
|
+
def test_collapses_whitespace(self):
|
|
2015
|
+
raw = "<system-reminder>X</system-reminder>\n\n 多余 空白\t\t折叠 "
|
|
2016
|
+
assert _sanitize_user_text(raw) == "多余 空白 折叠"
|
|
2017
|
+
|
|
2018
|
+
def test_empty_after_strip(self):
|
|
2019
|
+
raw = "<system-reminder>仅噪声</system-reminder>"
|
|
2020
|
+
assert _sanitize_user_text(raw) == ""
|
|
2021
|
+
|
|
2022
|
+
def test_empty_input(self):
|
|
2023
|
+
assert _sanitize_user_text("") == ""
|
|
2024
|
+
|
|
2025
|
+
def test_preserves_user_xml_like_content(self):
|
|
2026
|
+
"""用户输入中合法的 XML/HTML 片段(非白名单标签)需完整保留."""
|
|
2027
|
+
raw = "请帮我审查这段代码:<div>hello</div> 是否符合规范?"
|
|
2028
|
+
assert _sanitize_user_text(raw) == raw
|
|
2029
|
+
|
|
2030
|
+
def test_strips_local_command_output(self):
|
|
2031
|
+
raw = "<local-command-stdout>build ok</local-command-stdout>构建后的下一步问题"
|
|
2032
|
+
assert _sanitize_user_text(raw) == "构建后的下一步问题"
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
class TestExtractSessionTitle:
|
|
2036
|
+
"""``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题."""
|
|
2037
|
+
|
|
2038
|
+
@staticmethod
|
|
2039
|
+
def _build_request(messages: list[dict]):
|
|
2040
|
+
return build_canonical_request({"model": "test", "messages": messages}, {})
|
|
2041
|
+
|
|
2042
|
+
def test_truncates_to_max_len(self):
|
|
2043
|
+
long_text = "用户输入文本" * 20
|
|
2044
|
+
req = self._build_request([{"role": "user", "content": long_text}])
|
|
2045
|
+
title = _extract_session_title(req)
|
|
2046
|
+
assert len(title) == _SESSION_TITLE_MAX_LEN
|
|
2047
|
+
assert title == long_text[:_SESSION_TITLE_MAX_LEN]
|
|
2048
|
+
|
|
2049
|
+
def test_strips_noise_from_first_user_message(self):
|
|
2050
|
+
raw = (
|
|
2051
|
+
"<system-reminder>MCP 指令</system-reminder>"
|
|
2052
|
+
"<user-preferences>偏好</user-preferences>"
|
|
2053
|
+
"测试标题 ABC"
|
|
2054
|
+
)
|
|
2055
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2056
|
+
assert _extract_session_title(req) == "测试标题 ABC"
|
|
2057
|
+
|
|
2058
|
+
def test_handles_real_cc_first_message_shape(self):
|
|
2059
|
+
"""模拟 CC 真实首条消息(多个连续 system-reminder + 用户文本)."""
|
|
2060
|
+
raw = (
|
|
2061
|
+
"<system-reminder>\n# MCP Server Instructions\n...</system-reminder>"
|
|
2062
|
+
"<system-reminder>\nThe following skills...\n</system-reminder>"
|
|
2063
|
+
"<system-reminder>\nPlan mode is active...\n</system-reminder>"
|
|
2064
|
+
"\n\nTITLE 中的 Session 标题应当取自用户输入的信息前 30 个字\n\n"
|
|
2065
|
+
"<user-preferences>始终遵循 AGENTS.md</user-preferences>"
|
|
2066
|
+
)
|
|
2067
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2068
|
+
title = _extract_session_title(req)
|
|
2069
|
+
assert title.startswith("TITLE 中的 Session")
|
|
2070
|
+
assert len(title) <= _SESSION_TITLE_MAX_LEN
|
|
2071
|
+
|
|
2072
|
+
def test_extracts_slash_command(self):
|
|
2073
|
+
raw = (
|
|
2074
|
+
"<command-name>/commit</command-name>"
|
|
2075
|
+
"<command-args>feat: 新增标题清洗</command-args>"
|
|
2076
|
+
)
|
|
2077
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2078
|
+
assert _extract_session_title(req) == "/commit feat: 新增标题清洗"
|
|
2079
|
+
|
|
2080
|
+
def test_returns_empty_when_only_noise(self):
|
|
2081
|
+
raw = "<system-reminder>纯噪声</system-reminder>"
|
|
2082
|
+
req = self._build_request([{"role": "user", "content": raw}])
|
|
2083
|
+
assert _extract_session_title(req) == ""
|
|
2084
|
+
|
|
2085
|
+
def test_returns_empty_for_no_user_messages(self):
|
|
2086
|
+
req = self._build_request([{"role": "assistant", "content": "你好"}])
|
|
2087
|
+
assert _extract_session_title(req) == ""
|
|
2088
|
+
|
|
2089
|
+
def test_skips_noise_only_part_to_find_real_input(self):
|
|
2090
|
+
"""首个 user text part 全噪声时,fallback 到下一个非空 user part."""
|
|
2091
|
+
messages = [
|
|
2092
|
+
{
|
|
2093
|
+
"role": "user",
|
|
2094
|
+
"content": [
|
|
2095
|
+
{
|
|
2096
|
+
"type": "text",
|
|
2097
|
+
"text": "<system-reminder>noise</system-reminder>",
|
|
2098
|
+
},
|
|
2099
|
+
{"type": "text", "text": "真实问题"},
|
|
2100
|
+
],
|
|
2101
|
+
}
|
|
2102
|
+
]
|
|
2103
|
+
req = self._build_request(messages)
|
|
2104
|
+
assert _extract_session_title(req) == "真实问题"
|
|
2105
|
+
|
|
2106
|
+
def test_skips_assistant_role(self):
|
|
2107
|
+
"""assistant 角色的文本不应被作为标题候选."""
|
|
2108
|
+
messages = [
|
|
2109
|
+
{"role": "assistant", "content": "上一轮回答"},
|
|
2110
|
+
{"role": "user", "content": "新的用户问题"},
|
|
2111
|
+
]
|
|
2112
|
+
req = self._build_request(messages)
|
|
2113
|
+
assert _extract_session_title(req) == "新的用户问题"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|