coding-proxy 0.5.1a5__tar.gz → 0.5.1a7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/PKG-INFO +1 -1
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/pyproject.toml +1 -1
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/config.default.yaml +8 -1
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/schema.py +2 -1
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/session_policy.py +24 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/executor.py +77 -5
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/router.py +3 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/server/app.py +1 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_router_executor.py +324 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/uv.lock +1 -1
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/.gitignore +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/AGENTS.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/CHANGELOG.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/CLAUDE.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/LICENSE +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/README.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/assets/model-calling-v0.5.0.png +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/.agents/browser-validation.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/.agents/issue.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/.agents/knowledge-map.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/.agents/reference-specifications.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/arch/convert.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/arch/routing.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/arch/testing.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/framework.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/ops/ci-cd.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/user-guide.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/concurrency.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_banner.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_compat.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_concurrency_monitor.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_config_init.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_copilot.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_currency.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_executor_in_flight_tracking.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_mixins.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_token.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_pricing.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_schema.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_tier.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_time_range.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_types.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_vendors.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_zhipu.py +0 -0
- {coding_proxy-0.5.1a5 → coding_proxy-0.5.1a7}/tests/test_zhipu_concurrency.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1a7
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
6
|
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1a7"
|
|
4
4
|
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao..."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -685,4 +685,11 @@ native_api:
|
|
|
685
685
|
# tiers: ["copilot", "anthropic", "zhipu"]
|
|
686
686
|
#
|
|
687
687
|
# 未配置时(默认),所有 Session 使用全局 tiers 顺序。
|
|
688
|
-
session_policies:
|
|
688
|
+
session_policies:
|
|
689
|
+
policies: []
|
|
690
|
+
# 标题前缀 → 供应商自动绑定。
|
|
691
|
+
# 当 Session 标题以指定前缀开头时,自动将该 Session 绑定到对应供应商。
|
|
692
|
+
# 匹配规则按列表顺序求值,首次匹配生效。
|
|
693
|
+
title_vendor_bindings:
|
|
694
|
+
- prefix: "# 目标"
|
|
695
|
+
vendor: "zhipu"
|
|
@@ -44,7 +44,7 @@ from .routing import ( # noqa: F401
|
|
|
44
44
|
|
|
45
45
|
# ── 子模块 re-export ────────────────────────────────────────────
|
|
46
46
|
from .server import DatabaseConfig, LoggingConfig, ServerConfig # noqa: F401
|
|
47
|
-
from .session_policy import SessionPoliciesConfig # noqa: F401
|
|
47
|
+
from .session_policy import SessionPoliciesConfig, TitleVendorBinding # noqa: F401
|
|
48
48
|
from .vendors import ( # noqa: F401
|
|
49
49
|
AlibabaConfig,
|
|
50
50
|
AnthropicConfig,
|
|
@@ -350,4 +350,5 @@ __all__ = [
|
|
|
350
350
|
"NativeApiConfig",
|
|
351
351
|
# session policy
|
|
352
352
|
"SessionPoliciesConfig",
|
|
353
|
+
"TitleVendorBinding",
|
|
353
354
|
]
|
|
@@ -50,6 +50,22 @@ class SessionPolicy(BaseModel):
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
class TitleVendorBinding(BaseModel):
|
|
54
|
+
"""标题前缀 → 供应商自动绑定规则."""
|
|
55
|
+
|
|
56
|
+
prefix: str = Field(
|
|
57
|
+
min_length=1,
|
|
58
|
+
description=(
|
|
59
|
+
"标题前缀匹配模式(大小写敏感的 startswith 匹配)。"
|
|
60
|
+
"禁止空字符串——空前缀会匹配所有标题,导致全量误绑定。"
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
vendor: str = Field(
|
|
64
|
+
min_length=1,
|
|
65
|
+
description="匹配后绑定的目标供应商名称",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
53
69
|
class SessionPoliciesConfig(BaseModel):
|
|
54
70
|
"""顶层 Session 策略配置容器."""
|
|
55
71
|
|
|
@@ -57,3 +73,11 @@ class SessionPoliciesConfig(BaseModel):
|
|
|
57
73
|
default_factory=list,
|
|
58
74
|
description="Session 路由策略列表,按定义顺序求值,首次匹配生效",
|
|
59
75
|
)
|
|
76
|
+
title_vendor_bindings: list[TitleVendorBinding] = Field(
|
|
77
|
+
default_factory=list,
|
|
78
|
+
description=(
|
|
79
|
+
"标题前缀 → 供应商自动绑定规则。"
|
|
80
|
+
"当 Session 标题以指定前缀开头时,自动绑定到对应供应商。"
|
|
81
|
+
"匹配规则按列表顺序求值,首次匹配生效。"
|
|
82
|
+
),
|
|
83
|
+
)
|
|
@@ -11,10 +11,13 @@ import logging
|
|
|
11
11
|
import re
|
|
12
12
|
import time
|
|
13
13
|
from collections.abc import AsyncIterator
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..config.session_policy import TitleVendorBinding
|
|
20
|
+
|
|
18
21
|
from ..vendors.base import (
|
|
19
22
|
NoCompatibleVendorError,
|
|
20
23
|
RequestCapabilities,
|
|
@@ -71,6 +74,11 @@ _NOISE_TAG_PATTERN = re.compile(
|
|
|
71
74
|
flags=re.DOTALL | re.IGNORECASE,
|
|
72
75
|
)
|
|
73
76
|
|
|
77
|
+
# <session> 标签需要特殊处理:当用户文本在 <session> 标签内部时,
|
|
78
|
+
# 完整块剥离会连同用户文本一起删除。此模式仅去除外壳标签(保留内容),
|
|
79
|
+
# 用于首轮完整剥离结果为空时的二次回退提取。
|
|
80
|
+
_SESSION_TAG_WRAPPER = re.compile(r"</?session\b[^>]*>", flags=re.IGNORECASE)
|
|
81
|
+
|
|
74
82
|
# Slash command 子标签:用于识别 /commit、/review 等命令式调用,
|
|
75
83
|
# 合成"命令 + 参数"式标题。
|
|
76
84
|
_CMD_NAME_PATTERN = re.compile(r"<command-name>(.*?)</command-name>", flags=re.DOTALL)
|
|
@@ -80,6 +88,9 @@ _CMD_WRAPPER_PATTERN = re.compile(
|
|
|
80
88
|
r"<command-[\w-]+>.*?</command-[\w-]+>", flags=re.DOTALL
|
|
81
89
|
)
|
|
82
90
|
|
|
91
|
+
# 空白折叠
|
|
92
|
+
_WHITESPACE_PATTERN = re.compile(r"\s+")
|
|
93
|
+
|
|
83
94
|
|
|
84
95
|
def _sanitize_user_text(raw: str) -> str:
|
|
85
96
|
"""剔除 Claude Code 注入的系统级 XML 块,还原真实用户输入。
|
|
@@ -88,8 +99,10 @@ def _sanitize_user_text(raw: str) -> str:
|
|
|
88
99
|
1. Slash command 优先识别 — 若检测到 <command-name>,合成"命令 + 参数"
|
|
89
100
|
式标题(因为残留文本通常为空,直接取标签内容更有意义)。
|
|
90
101
|
2. 通用噪声剥离 — 移除已知白名单内的 system-reminder 等标签。
|
|
91
|
-
3.
|
|
92
|
-
|
|
102
|
+
3. <session> 二次回退 — 若首轮剥离后为空,说明用户文本可能在 <session>
|
|
103
|
+
标签内部;此时仅去除外壳标签,保留内部文本再做噪声剥离。
|
|
104
|
+
4. 残留 command-* 包裹清除 — 兜底去除 command-message 等次要标签。
|
|
105
|
+
5. 前后空白归一化 — 折叠连续空白为单空格。
|
|
93
106
|
"""
|
|
94
107
|
if not raw:
|
|
95
108
|
return ""
|
|
@@ -107,9 +120,22 @@ def _sanitize_user_text(raw: str) -> str:
|
|
|
107
120
|
# 阶段二: 通用噪声剥离
|
|
108
121
|
cleaned = _NOISE_TAG_PATTERN.sub("", raw)
|
|
109
122
|
cleaned = _CMD_WRAPPER_PATTERN.sub("", cleaned)
|
|
123
|
+
cleaned = _WHITESPACE_PATTERN.sub(" ", cleaned).strip()
|
|
124
|
+
if cleaned:
|
|
125
|
+
return cleaned
|
|
126
|
+
|
|
127
|
+
# 阶段三: <session> 二次回退
|
|
128
|
+
# 当首轮全部剥离为空时,用户文本很可能被 <session> 标签完整包裹。
|
|
129
|
+
# 此时不去除 <session> 块,而是仅剥掉外壳标签,保留内部文本后重新剥离。
|
|
130
|
+
if "<session" in raw.lower():
|
|
131
|
+
inner = _SESSION_TAG_WRAPPER.sub("", raw)
|
|
132
|
+
cleaned = _NOISE_TAG_PATTERN.sub("", inner)
|
|
133
|
+
cleaned = _CMD_WRAPPER_PATTERN.sub("", cleaned)
|
|
134
|
+
cleaned = _WHITESPACE_PATTERN.sub(" ", cleaned).strip()
|
|
135
|
+
if cleaned:
|
|
136
|
+
return cleaned
|
|
110
137
|
|
|
111
|
-
|
|
112
|
-
return re.sub(r"\s+", " ", cleaned).strip()
|
|
138
|
+
return ""
|
|
113
139
|
|
|
114
140
|
|
|
115
141
|
# ── Session 标题提取: 多层级回退策略 ──────────────────────────────
|
|
@@ -587,6 +613,7 @@ class _RouteExecutor:
|
|
|
587
613
|
session_manager: RouteSessionManager,
|
|
588
614
|
reauth_coordinator: Any | None = None,
|
|
589
615
|
session_policy_resolver: SessionPolicyResolver | None = None,
|
|
616
|
+
title_vendor_bindings: list[TitleVendorBinding] | None = None,
|
|
590
617
|
) -> None:
|
|
591
618
|
self._router = router
|
|
592
619
|
self._tiers = tiers
|
|
@@ -594,6 +621,8 @@ class _RouteExecutor:
|
|
|
594
621
|
self._session_mgr = session_manager
|
|
595
622
|
self._reauth_coordinator = reauth_coordinator
|
|
596
623
|
self._policy_resolver = session_policy_resolver or SessionPolicyResolver()
|
|
624
|
+
self._title_vendor_bindings = title_vendor_bindings or []
|
|
625
|
+
self._validate_title_vendor_bindings()
|
|
597
626
|
|
|
598
627
|
# Tier 名称 → OAuth provider 名称的映射
|
|
599
628
|
self._tier_provider_map: dict[str, str] = {
|
|
@@ -601,6 +630,26 @@ class _RouteExecutor:
|
|
|
601
630
|
"antigravity": "google",
|
|
602
631
|
}
|
|
603
632
|
|
|
633
|
+
def _validate_title_vendor_bindings(self) -> None:
|
|
634
|
+
"""启动期校验标题绑定引用的 vendor 均存在,缺失则告警.
|
|
635
|
+
|
|
636
|
+
与手动绑定 API(拒绝未知 vendor)的语义对齐:此处不硬失败,
|
|
637
|
+
仅记录警告——避免单条误配置阻断整个代理启动;运行时
|
|
638
|
+
`_resolve_effective_tiers` 会静默跳过未知 vendor 回退默认顺序。
|
|
639
|
+
"""
|
|
640
|
+
if not self._title_vendor_bindings:
|
|
641
|
+
return
|
|
642
|
+
valid = {t.name for t in self._tiers}
|
|
643
|
+
for binding in self._title_vendor_bindings:
|
|
644
|
+
if binding.vendor not in valid:
|
|
645
|
+
logger.warning(
|
|
646
|
+
"title_vendor_bindings 引用了未知 vendor %r(前缀 %r);"
|
|
647
|
+
"可用 vendor: %s。该绑定将在运行时被静默跳过。",
|
|
648
|
+
binding.vendor,
|
|
649
|
+
binding.prefix,
|
|
650
|
+
sorted(valid),
|
|
651
|
+
)
|
|
652
|
+
|
|
604
653
|
# ── 公开执行入口 ──────────────────────────────────────
|
|
605
654
|
|
|
606
655
|
def _resolve_effective_tiers(self, session_key: str) -> list[VendorTier]:
|
|
@@ -627,6 +676,27 @@ class _RouteExecutor:
|
|
|
627
676
|
seen.add(tier.name)
|
|
628
677
|
return ordered
|
|
629
678
|
|
|
679
|
+
def _apply_title_based_policy(self, session_key: str, title: str) -> None:
|
|
680
|
+
"""根据 Session 标题前缀自动绑定供应商.
|
|
681
|
+
|
|
682
|
+
当标题以预配置的前缀开头时,通过 SessionPolicyResolver.upsert()
|
|
683
|
+
将该 Session 绑定到指定供应商,后续请求无需再走默认路由。
|
|
684
|
+
|
|
685
|
+
仅在新 Session 首次提取标题时调用,避免覆盖手动绑定的策略。
|
|
686
|
+
"""
|
|
687
|
+
if not title or not self._title_vendor_bindings:
|
|
688
|
+
return
|
|
689
|
+
for binding in self._title_vendor_bindings:
|
|
690
|
+
if title.startswith(binding.prefix):
|
|
691
|
+
self._policy_resolver.upsert(session_key, [binding.vendor])
|
|
692
|
+
logger.info(
|
|
693
|
+
"Session title prefix %r matched → auto-bind to %s (session=%s)",
|
|
694
|
+
binding.prefix,
|
|
695
|
+
binding.vendor,
|
|
696
|
+
session_key[:12],
|
|
697
|
+
)
|
|
698
|
+
return
|
|
699
|
+
|
|
630
700
|
def _prepare_body_for_tier(
|
|
631
701
|
self,
|
|
632
702
|
body: dict[str, Any],
|
|
@@ -725,6 +795,7 @@ class _RouteExecutor:
|
|
|
725
795
|
await self._recorder.set_session_title(
|
|
726
796
|
canonical_request.session_key, title
|
|
727
797
|
)
|
|
798
|
+
self._apply_title_based_policy(canonical_request.session_key, title)
|
|
728
799
|
else:
|
|
729
800
|
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
|
|
730
801
|
title = _extract_session_title(canonical_request)
|
|
@@ -911,6 +982,7 @@ class _RouteExecutor:
|
|
|
911
982
|
await self._recorder.set_session_title(
|
|
912
983
|
canonical_request.session_key, title
|
|
913
984
|
)
|
|
985
|
+
self._apply_title_based_policy(canonical_request.session_key, title)
|
|
914
986
|
else:
|
|
915
987
|
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
|
|
916
988
|
title = _extract_session_title(canonical_request)
|
|
@@ -14,6 +14,7 @@ from collections.abc import AsyncIterator
|
|
|
14
14
|
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
+
from ..config.session_policy import TitleVendorBinding
|
|
17
18
|
from ..pricing import PricingTable
|
|
18
19
|
|
|
19
20
|
from .executor import _RouteExecutor
|
|
@@ -38,6 +39,7 @@ class RequestRouter:
|
|
|
38
39
|
reauth_coordinator: Any | None = None,
|
|
39
40
|
compat_session_store: CompatSessionStore | None = None,
|
|
40
41
|
session_policy_resolver: SessionPolicyResolver | None = None,
|
|
42
|
+
title_vendor_bindings: list[TitleVendorBinding] | None = None,
|
|
41
43
|
) -> None:
|
|
42
44
|
if not tiers:
|
|
43
45
|
raise ValueError("至少需要一个供应商层级")
|
|
@@ -56,6 +58,7 @@ class RequestRouter:
|
|
|
56
58
|
session_manager=self._session_mgr,
|
|
57
59
|
reauth_coordinator=reauth_coordinator,
|
|
58
60
|
session_policy_resolver=session_policy_resolver,
|
|
61
|
+
title_vendor_bindings=title_vendor_bindings,
|
|
59
62
|
)
|
|
60
63
|
|
|
61
64
|
def set_pricing_table(self, table: PricingTable) -> None:
|
|
@@ -161,6 +161,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
|
|
|
161
161
|
reauth_coordinator,
|
|
162
162
|
compat_session_store,
|
|
163
163
|
session_policy_resolver=SessionPolicyResolver(config.session_policies.policies),
|
|
164
|
+
title_vendor_bindings=config.session_policies.title_vendor_bindings,
|
|
164
165
|
)
|
|
165
166
|
|
|
166
167
|
app = FastAPI(title="coding-proxy", version=__version__, lifespan=lifespan)
|
|
@@ -19,6 +19,7 @@ from coding.proxy.compat.canonical import (
|
|
|
19
19
|
CompatibilityStatus,
|
|
20
20
|
build_canonical_request,
|
|
21
21
|
)
|
|
22
|
+
from coding.proxy.config.session_policy import TitleVendorBinding
|
|
22
23
|
from coding.proxy.routing.executor import (
|
|
23
24
|
_FALLBACK_TITLE_MAX_LEN,
|
|
24
25
|
_SESSION_TITLE_MAX_LEN,
|
|
@@ -36,6 +37,7 @@ from coding.proxy.routing.executor import (
|
|
|
36
37
|
_sanitize_user_text,
|
|
37
38
|
)
|
|
38
39
|
from coding.proxy.routing.session_manager import RouteSessionManager
|
|
40
|
+
from coding.proxy.routing.session_policy import SessionPolicyResolver
|
|
39
41
|
from coding.proxy.routing.tier import VendorTier
|
|
40
42
|
from coding.proxy.routing.usage_recorder import UsageRecorder
|
|
41
43
|
from coding.proxy.vendors.base import (
|
|
@@ -133,6 +135,19 @@ def _executor(tiers: list[VendorTier] | None = None, **kwargs) -> _RouteExecutor
|
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
def _stub_session_manager(is_new: bool = True) -> MagicMock:
|
|
139
|
+
"""构造返回指定 is_new 的 session manager stub.
|
|
140
|
+
|
|
141
|
+
默认 RouteSessionManager(无 store) 的 get_or_create_record 恒返回
|
|
142
|
+
is_new=False;测试新 session 路径需显式 stub 返回 is_new=True。
|
|
143
|
+
"""
|
|
144
|
+
mgr = MagicMock(spec=RouteSessionManager)
|
|
145
|
+
mgr.get_or_create_record = AsyncMock(return_value=(None, is_new))
|
|
146
|
+
mgr.apply_compat_context = MagicMock()
|
|
147
|
+
mgr.persist_session = AsyncMock()
|
|
148
|
+
return mgr
|
|
149
|
+
|
|
150
|
+
|
|
136
151
|
# ── _VENDOR_PROTOCOL_LABEL_MAP ───────────────────────────
|
|
137
152
|
|
|
138
153
|
|
|
@@ -2277,6 +2292,40 @@ class TestSanitizeUserText:
|
|
|
2277
2292
|
raw = "<thinking>\nline1\nline2\n</thinking>清理后文本"
|
|
2278
2293
|
assert _sanitize_user_text(raw) == "清理后文本"
|
|
2279
2294
|
|
|
2295
|
+
# ── <session> 标签包裹用户文本的二次回退 ──
|
|
2296
|
+
|
|
2297
|
+
def test_session_tag_wrapping_user_text(self):
|
|
2298
|
+
"""当 <session> 标签包裹用户文本时,二次回退应提取内部文本.
|
|
2299
|
+
|
|
2300
|
+
注: session 元数据可能残留在标题前部,但用户文本现在可见,
|
|
2301
|
+
远优于完全回退到 '[Session] model_name'.
|
|
2302
|
+
"""
|
|
2303
|
+
raw = "<session>session metadata\n用户真实提问内容</session>"
|
|
2304
|
+
result = _sanitize_user_text(raw)
|
|
2305
|
+
assert "用户真实提问内容" in result
|
|
2306
|
+
|
|
2307
|
+
def test_session_tag_wrapping_with_inner_noise(self):
|
|
2308
|
+
"""<session> 内部混合噪声标签时,二次回退应正确剥离噪声."""
|
|
2309
|
+
raw = (
|
|
2310
|
+
"<session>session_key: abc\n"
|
|
2311
|
+
"<system-reminder>噪声内容</system-reminder>"
|
|
2312
|
+
"用户真实输入"
|
|
2313
|
+
"</session>"
|
|
2314
|
+
)
|
|
2315
|
+
result = _sanitize_user_text(raw)
|
|
2316
|
+
assert "用户真实输入" in result
|
|
2317
|
+
assert "噪声内容" not in result
|
|
2318
|
+
|
|
2319
|
+
def test_session_tag_prefix_still_works(self):
|
|
2320
|
+
"""用户文本在 <session> 标签之后(原有行为)仍正确."""
|
|
2321
|
+
raw = "<session>metadata</session>用户文本在外部"
|
|
2322
|
+
assert _sanitize_user_text(raw) == "用户文本在外部"
|
|
2323
|
+
|
|
2324
|
+
def test_all_noise_inside_session_tag(self):
|
|
2325
|
+
"""<session> 内部全是噪声时,二次回退仍返回空."""
|
|
2326
|
+
raw = "<session><system-reminder>纯噪声</system-reminder></session>"
|
|
2327
|
+
assert _sanitize_user_text(raw) == ""
|
|
2328
|
+
|
|
2280
2329
|
|
|
2281
2330
|
class TestExtractSessionTitle:
|
|
2282
2331
|
"""``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题."""
|
|
@@ -2622,3 +2671,278 @@ class TestExtractSessionTitleFallback:
|
|
|
2622
2671
|
]
|
|
2623
2672
|
req = self._build_request(messages)
|
|
2624
2673
|
assert _extract_session_title(req) == "[Session] test-model"
|
|
2674
|
+
|
|
2675
|
+
|
|
2676
|
+
class TestApplyTitleBasedPolicy:
|
|
2677
|
+
"""``_apply_title_based_policy`` 标题前缀自动绑定测试."""
|
|
2678
|
+
|
|
2679
|
+
def test_prefix_match_triggers_upsert(self):
|
|
2680
|
+
"""标题以配置前缀开头 → 触发 upsert 绑定到目标 vendor."""
|
|
2681
|
+
resolver = SessionPolicyResolver()
|
|
2682
|
+
executor = _executor(
|
|
2683
|
+
session_policy_resolver=resolver,
|
|
2684
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2685
|
+
)
|
|
2686
|
+
executor._apply_title_based_policy("sess-1", "# 目标 (Goal) 实现功能 X")
|
|
2687
|
+
policy = resolver.resolve("sess-1")
|
|
2688
|
+
assert policy is not None
|
|
2689
|
+
assert policy.tiers == ["zhipu"]
|
|
2690
|
+
assert policy.name == "runtime:sess-1"
|
|
2691
|
+
|
|
2692
|
+
def test_prefix_match_without_parenthesis(self):
|
|
2693
|
+
"""前缀匹配不要求括号后缀,纯 '# 目标' 开头即命中."""
|
|
2694
|
+
resolver = SessionPolicyResolver()
|
|
2695
|
+
executor = _executor(
|
|
2696
|
+
session_policy_resolver=resolver,
|
|
2697
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2698
|
+
)
|
|
2699
|
+
executor._apply_title_based_policy("sess-2", "# 目标 详细计划")
|
|
2700
|
+
policy = resolver.resolve("sess-2")
|
|
2701
|
+
assert policy is not None
|
|
2702
|
+
assert policy.tiers == ["zhipu"]
|
|
2703
|
+
|
|
2704
|
+
def test_non_matching_title_no_binding(self):
|
|
2705
|
+
"""非匹配标题 → 不创建绑定."""
|
|
2706
|
+
resolver = SessionPolicyResolver()
|
|
2707
|
+
executor = _executor(
|
|
2708
|
+
session_policy_resolver=resolver,
|
|
2709
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2710
|
+
)
|
|
2711
|
+
executor._apply_title_based_policy("sess-3", "普通会话标题")
|
|
2712
|
+
assert resolver.resolve("sess-3") is None
|
|
2713
|
+
|
|
2714
|
+
def test_empty_title_no_binding(self):
|
|
2715
|
+
"""空标题 → 提前返回,不创建绑定."""
|
|
2716
|
+
resolver = SessionPolicyResolver()
|
|
2717
|
+
executor = _executor(
|
|
2718
|
+
session_policy_resolver=resolver,
|
|
2719
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2720
|
+
)
|
|
2721
|
+
executor._apply_title_based_policy("sess-4", "")
|
|
2722
|
+
assert resolver.resolve("sess-4") is None
|
|
2723
|
+
|
|
2724
|
+
def test_no_bindings_configured_no_binding(self):
|
|
2725
|
+
"""未配置任何绑定规则 → 提前返回,等效禁用."""
|
|
2726
|
+
resolver = SessionPolicyResolver()
|
|
2727
|
+
executor = _executor(
|
|
2728
|
+
session_policy_resolver=resolver,
|
|
2729
|
+
title_vendor_bindings=[],
|
|
2730
|
+
)
|
|
2731
|
+
executor._apply_title_based_policy("sess-5", "# 目标 任意标题")
|
|
2732
|
+
assert resolver.resolve("sess-5") is None
|
|
2733
|
+
|
|
2734
|
+
def test_prefix_in_middle_no_match(self):
|
|
2735
|
+
"""前缀出现在标题中间 → startswith 不匹配,不绑定."""
|
|
2736
|
+
resolver = SessionPolicyResolver()
|
|
2737
|
+
executor = _executor(
|
|
2738
|
+
session_policy_resolver=resolver,
|
|
2739
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2740
|
+
)
|
|
2741
|
+
executor._apply_title_based_policy("sess-6", "前缀 # 目标 在中间")
|
|
2742
|
+
assert resolver.resolve("sess-6") is None
|
|
2743
|
+
|
|
2744
|
+
def test_multiple_bindings_first_match_wins(self):
|
|
2745
|
+
"""多条规则按顺序匹配,首次命中生效."""
|
|
2746
|
+
resolver = SessionPolicyResolver()
|
|
2747
|
+
executor = _executor(
|
|
2748
|
+
session_policy_resolver=resolver,
|
|
2749
|
+
title_vendor_bindings=[
|
|
2750
|
+
TitleVendorBinding(prefix="# 目标", vendor="zhipu"),
|
|
2751
|
+
TitleVendorBinding(prefix="# Review", vendor="anthropic"),
|
|
2752
|
+
],
|
|
2753
|
+
)
|
|
2754
|
+
executor._apply_title_based_policy("sess-7", "# Review 代码审查")
|
|
2755
|
+
policy = resolver.resolve("sess-7")
|
|
2756
|
+
assert policy is not None
|
|
2757
|
+
assert policy.tiers == ["anthropic"]
|
|
2758
|
+
|
|
2759
|
+
def test_bound_tier_promoted_to_front(self):
|
|
2760
|
+
"""绑定后 _resolve_effective_tiers 将目标 vendor 提升至首位."""
|
|
2761
|
+
resolver = SessionPolicyResolver()
|
|
2762
|
+
tiers = [
|
|
2763
|
+
_make_tier(_mock_vendor("anthropic")),
|
|
2764
|
+
_make_tier(_mock_vendor("zhipu")),
|
|
2765
|
+
]
|
|
2766
|
+
executor = _executor(
|
|
2767
|
+
tiers=tiers,
|
|
2768
|
+
session_policy_resolver=resolver,
|
|
2769
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2770
|
+
)
|
|
2771
|
+
executor._apply_title_based_policy("sess-8", "# 目标 实现 X")
|
|
2772
|
+
effective = executor._resolve_effective_tiers("sess-8")
|
|
2773
|
+
assert effective[0].name == "zhipu"
|
|
2774
|
+
# 未提及的 vendor 仍保留在末尾
|
|
2775
|
+
assert {t.name for t in effective} == {"zhipu", "anthropic"}
|
|
2776
|
+
|
|
2777
|
+
def test_non_matching_session_uses_default_order(self):
|
|
2778
|
+
"""非匹配 session 的 tier 顺序保持全局默认."""
|
|
2779
|
+
resolver = SessionPolicyResolver()
|
|
2780
|
+
tiers = [
|
|
2781
|
+
_make_tier(_mock_vendor("anthropic")),
|
|
2782
|
+
_make_tier(_mock_vendor("zhipu")),
|
|
2783
|
+
]
|
|
2784
|
+
executor = _executor(
|
|
2785
|
+
tiers=tiers,
|
|
2786
|
+
session_policy_resolver=resolver,
|
|
2787
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2788
|
+
)
|
|
2789
|
+
executor._apply_title_based_policy("sess-9", "普通标题")
|
|
2790
|
+
effective = executor._resolve_effective_tiers("sess-9")
|
|
2791
|
+
assert [t.name for t in effective] == ["anthropic", "zhipu"]
|
|
2792
|
+
|
|
2793
|
+
def test_nonexistent_vendor_skipped_in_resolution(self):
|
|
2794
|
+
"""绑定不存在的 vendor → upsert 成功但 tier 解析跳过该 vendor."""
|
|
2795
|
+
resolver = SessionPolicyResolver()
|
|
2796
|
+
tiers = [
|
|
2797
|
+
_make_tier(_mock_vendor("anthropic")),
|
|
2798
|
+
_make_tier(_mock_vendor("zhipu")),
|
|
2799
|
+
]
|
|
2800
|
+
executor = _executor(
|
|
2801
|
+
tiers=tiers,
|
|
2802
|
+
session_policy_resolver=resolver,
|
|
2803
|
+
title_vendor_bindings=[
|
|
2804
|
+
TitleVendorBinding(prefix="# 目标", vendor="nonexistent")
|
|
2805
|
+
],
|
|
2806
|
+
)
|
|
2807
|
+
executor._apply_title_based_policy("sess-10", "# 目标 X")
|
|
2808
|
+
effective = executor._resolve_effective_tiers("sess-10")
|
|
2809
|
+
# 不存在的 vendor 被跳过,回退到全局默认顺序
|
|
2810
|
+
assert [t.name for t in effective] == ["anthropic", "zhipu"]
|
|
2811
|
+
|
|
2812
|
+
@pytest.mark.asyncio
|
|
2813
|
+
async def test_execute_message_end_to_end_binding(self):
|
|
2814
|
+
"""端到端: 新 session 首请求标题命中前缀 → 创建绑定并路由到 zhipu."""
|
|
2815
|
+
resolver = SessionPolicyResolver()
|
|
2816
|
+
tiers = [
|
|
2817
|
+
_make_tier(_mock_vendor("anthropic")),
|
|
2818
|
+
_make_tier(_mock_vendor("zhipu")),
|
|
2819
|
+
]
|
|
2820
|
+
executor = _executor(
|
|
2821
|
+
tiers=tiers,
|
|
2822
|
+
session_mgr=_stub_session_manager(is_new=True),
|
|
2823
|
+
session_policy_resolver=resolver,
|
|
2824
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2825
|
+
)
|
|
2826
|
+
body = {
|
|
2827
|
+
"model": "test",
|
|
2828
|
+
"metadata": {"user_id": "session-abc"},
|
|
2829
|
+
"messages": [
|
|
2830
|
+
{"role": "user", "content": [{"type": "text", "text": "# 目标 实现 X"}]}
|
|
2831
|
+
],
|
|
2832
|
+
}
|
|
2833
|
+
resp = await executor.execute_message(body, {})
|
|
2834
|
+
assert resp.status_code == 200
|
|
2835
|
+
# 从 body 解析出的 session_key 应已建立运行时绑定
|
|
2836
|
+
canonical = build_canonical_request(body, {})
|
|
2837
|
+
policy = resolver.resolve(canonical.session_key)
|
|
2838
|
+
assert policy is not None
|
|
2839
|
+
assert policy.tiers == ["zhipu"]
|
|
2840
|
+
|
|
2841
|
+
@pytest.mark.asyncio
|
|
2842
|
+
async def test_execute_message_existing_session_no_binding(self):
|
|
2843
|
+
"""端到端: 已存在 session(is_new=False) 不触发标题绑定."""
|
|
2844
|
+
resolver = SessionPolicyResolver()
|
|
2845
|
+
tiers = [
|
|
2846
|
+
_make_tier(_mock_vendor("anthropic")),
|
|
2847
|
+
_make_tier(_mock_vendor("zhipu")),
|
|
2848
|
+
]
|
|
2849
|
+
executor = _executor(
|
|
2850
|
+
tiers=tiers,
|
|
2851
|
+
session_mgr=_stub_session_manager(is_new=False),
|
|
2852
|
+
session_policy_resolver=resolver,
|
|
2853
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2854
|
+
)
|
|
2855
|
+
body = {
|
|
2856
|
+
"model": "test",
|
|
2857
|
+
"metadata": {"user_id": "session-existing"},
|
|
2858
|
+
"messages": [
|
|
2859
|
+
{"role": "user", "content": [{"type": "text", "text": "# 目标 实现 X"}]}
|
|
2860
|
+
],
|
|
2861
|
+
}
|
|
2862
|
+
await executor.execute_message(body, {})
|
|
2863
|
+
canonical = build_canonical_request(body, {})
|
|
2864
|
+
# is_new=False → 不调用 _apply_title_based_policy,无运行时绑定
|
|
2865
|
+
assert resolver.resolve(canonical.session_key) is None
|
|
2866
|
+
|
|
2867
|
+
@pytest.mark.asyncio
|
|
2868
|
+
async def test_execute_stream_end_to_end_binding(self):
|
|
2869
|
+
"""端到端(流式): 新 session 首请求标题命中前缀 → 创建绑定."""
|
|
2870
|
+
resolver = SessionPolicyResolver()
|
|
2871
|
+
zhipu_vendor = _mock_vendor("zhipu")
|
|
2872
|
+
zhipu_vendor.send_message_stream = MagicMock(
|
|
2873
|
+
return_value=_async_chunks([b'{"type":"message_stop"}'])
|
|
2874
|
+
)
|
|
2875
|
+
tiers = [
|
|
2876
|
+
_make_tier(_mock_vendor("anthropic")),
|
|
2877
|
+
_make_tier(zhipu_vendor),
|
|
2878
|
+
]
|
|
2879
|
+
executor = _executor(
|
|
2880
|
+
tiers=tiers,
|
|
2881
|
+
session_mgr=_stub_session_manager(is_new=True),
|
|
2882
|
+
session_policy_resolver=resolver,
|
|
2883
|
+
title_vendor_bindings=[TitleVendorBinding(prefix="# 目标", vendor="zhipu")],
|
|
2884
|
+
)
|
|
2885
|
+
body = {
|
|
2886
|
+
"model": "test",
|
|
2887
|
+
"metadata": {"user_id": "session-stream"},
|
|
2888
|
+
"messages": [
|
|
2889
|
+
{
|
|
2890
|
+
"role": "user",
|
|
2891
|
+
"content": [{"type": "text", "text": "# 目标 流式任务"}],
|
|
2892
|
+
}
|
|
2893
|
+
],
|
|
2894
|
+
}
|
|
2895
|
+
chunks = [chunk async for chunk, _ in executor.execute_stream(body, {})]
|
|
2896
|
+
assert chunks # 有数据返回
|
|
2897
|
+
canonical = build_canonical_request(body, {})
|
|
2898
|
+
policy = resolver.resolve(canonical.session_key)
|
|
2899
|
+
assert policy is not None
|
|
2900
|
+
assert policy.tiers == ["zhipu"]
|
|
2901
|
+
|
|
2902
|
+
def test_empty_prefix_rejected_by_validation(self):
|
|
2903
|
+
"""空 prefix 在模型校验阶段即被拒绝,杜绝全量误绑定."""
|
|
2904
|
+
import pydantic
|
|
2905
|
+
|
|
2906
|
+
with pytest.raises(pydantic.ValidationError):
|
|
2907
|
+
TitleVendorBinding(prefix="", vendor="zhipu")
|
|
2908
|
+
|
|
2909
|
+
def test_empty_vendor_rejected_by_validation(self):
|
|
2910
|
+
"""空 vendor 在模型校验阶段即被拒绝."""
|
|
2911
|
+
import pydantic
|
|
2912
|
+
|
|
2913
|
+
with pytest.raises(pydantic.ValidationError):
|
|
2914
|
+
TitleVendorBinding(prefix="# 目标", vendor="")
|
|
2915
|
+
|
|
2916
|
+
def test_unknown_vendor_warns_at_startup(self, caplog):
|
|
2917
|
+
"""构造时引用未知 vendor → 记录启动告警."""
|
|
2918
|
+
import logging as _logging
|
|
2919
|
+
|
|
2920
|
+
tiers = [_make_tier(_mock_vendor("anthropic"))]
|
|
2921
|
+
with caplog.at_level(_logging.WARNING, logger="coding.proxy.routing.executor"):
|
|
2922
|
+
_executor(
|
|
2923
|
+
tiers=tiers,
|
|
2924
|
+
session_policy_resolver=SessionPolicyResolver(),
|
|
2925
|
+
title_vendor_bindings=[
|
|
2926
|
+
TitleVendorBinding(prefix="# 目标", vendor="nonexistent")
|
|
2927
|
+
],
|
|
2928
|
+
)
|
|
2929
|
+
warnings = [r for r in caplog.records if r.levelno == _logging.WARNING]
|
|
2930
|
+
assert any("nonexistent" in r.message for r in warnings)
|
|
2931
|
+
|
|
2932
|
+
def test_known_vendor_no_startup_warning(self, caplog):
|
|
2933
|
+
"""构造时引用合法 vendor → 不产生告警."""
|
|
2934
|
+
import logging as _logging
|
|
2935
|
+
|
|
2936
|
+
tiers = [_make_tier(_mock_vendor("zhipu"))]
|
|
2937
|
+
with caplog.at_level(_logging.WARNING, logger="coding.proxy.routing.executor"):
|
|
2938
|
+
_executor(
|
|
2939
|
+
tiers=tiers,
|
|
2940
|
+
session_policy_resolver=SessionPolicyResolver(),
|
|
2941
|
+
title_vendor_bindings=[
|
|
2942
|
+
TitleVendorBinding(prefix="# 目标", vendor="zhipu")
|
|
2943
|
+
],
|
|
2944
|
+
)
|
|
2945
|
+
binding_warnings = [
|
|
2946
|
+
r for r in caplog.records if "title_vendor_bindings" in r.message
|
|
2947
|
+
]
|
|
2948
|
+
assert not binding_warnings
|
|
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
|