coding-proxy 0.5.1a6__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.1a6 → coding_proxy-0.5.1a7}/PKG-INFO +1 -1
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/pyproject.toml +1 -1
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/config.default.yaml +8 -1
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/schema.py +2 -1
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/session_policy.py +24 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/executor.py +50 -1
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/router.py +3 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/server/app.py +1 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_router_executor.py +290 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/uv.lock +1 -1
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/.gitignore +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/AGENTS.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/CHANGELOG.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/CLAUDE.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/LICENSE +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/README.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/assets/model-calling-v0.5.0.png +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/.agents/browser-validation.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/.agents/issue.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/.agents/knowledge-map.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/.agents/reference-specifications.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/arch/convert.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/arch/routing.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/arch/testing.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/framework.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/ops/ci-cd.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/user-guide.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/concurrency.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_banner.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_compat.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_concurrency_monitor.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_config_init.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_copilot.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_currency.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_executor_in_flight_tracking.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_mixins.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_token.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_pricing.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_schema.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_tier.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_time_range.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_types.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_vendors.py +0 -0
- {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a7}/tests/test_zhipu.py +0 -0
- {coding_proxy-0.5.1a6 → 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,
|
|
@@ -610,6 +613,7 @@ class _RouteExecutor:
|
|
|
610
613
|
session_manager: RouteSessionManager,
|
|
611
614
|
reauth_coordinator: Any | None = None,
|
|
612
615
|
session_policy_resolver: SessionPolicyResolver | None = None,
|
|
616
|
+
title_vendor_bindings: list[TitleVendorBinding] | None = None,
|
|
613
617
|
) -> None:
|
|
614
618
|
self._router = router
|
|
615
619
|
self._tiers = tiers
|
|
@@ -617,6 +621,8 @@ class _RouteExecutor:
|
|
|
617
621
|
self._session_mgr = session_manager
|
|
618
622
|
self._reauth_coordinator = reauth_coordinator
|
|
619
623
|
self._policy_resolver = session_policy_resolver or SessionPolicyResolver()
|
|
624
|
+
self._title_vendor_bindings = title_vendor_bindings or []
|
|
625
|
+
self._validate_title_vendor_bindings()
|
|
620
626
|
|
|
621
627
|
# Tier 名称 → OAuth provider 名称的映射
|
|
622
628
|
self._tier_provider_map: dict[str, str] = {
|
|
@@ -624,6 +630,26 @@ class _RouteExecutor:
|
|
|
624
630
|
"antigravity": "google",
|
|
625
631
|
}
|
|
626
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
|
+
|
|
627
653
|
# ── 公开执行入口 ──────────────────────────────────────
|
|
628
654
|
|
|
629
655
|
def _resolve_effective_tiers(self, session_key: str) -> list[VendorTier]:
|
|
@@ -650,6 +676,27 @@ class _RouteExecutor:
|
|
|
650
676
|
seen.add(tier.name)
|
|
651
677
|
return ordered
|
|
652
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
|
+
|
|
653
700
|
def _prepare_body_for_tier(
|
|
654
701
|
self,
|
|
655
702
|
body: dict[str, Any],
|
|
@@ -748,6 +795,7 @@ class _RouteExecutor:
|
|
|
748
795
|
await self._recorder.set_session_title(
|
|
749
796
|
canonical_request.session_key, title
|
|
750
797
|
)
|
|
798
|
+
self._apply_title_based_policy(canonical_request.session_key, title)
|
|
751
799
|
else:
|
|
752
800
|
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
|
|
753
801
|
title = _extract_session_title(canonical_request)
|
|
@@ -934,6 +982,7 @@ class _RouteExecutor:
|
|
|
934
982
|
await self._recorder.set_session_title(
|
|
935
983
|
canonical_request.session_key, title
|
|
936
984
|
)
|
|
985
|
+
self._apply_title_based_policy(canonical_request.session_key, title)
|
|
937
986
|
else:
|
|
938
987
|
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
|
|
939
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
|
|
|
@@ -2656,3 +2671,278 @@ class TestExtractSessionTitleFallback:
|
|
|
2656
2671
|
]
|
|
2657
2672
|
req = self._build_request(messages)
|
|
2658
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
|
|
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
|