coding-proxy 0.3.1a8__tar.gz → 0.3.1a9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/PKG-INFO +1 -1
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/pyproject.toml +1 -1
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/cli/__init__.py +95 -0
- coding_proxy-0.3.1a9/src/coding/proxy/routing/session_policy.py +116 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/server/dashboard.py +225 -33
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/server/routes.py +91 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_session_aware.py +141 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/uv.lock +1 -1
- coding_proxy-0.3.1a8/src/coding/proxy/routing/session_policy.py +0 -56
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/.gitignore +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/AGENTS.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/CHANGELOG.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/LICENSE +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/README.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/assets/dashboard-v0.2.4.png +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/issue.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_router_executor.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_types.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_vendors.py +0 -0
- {coding_proxy-0.3.1a8 → coding_proxy-0.3.1a9}/tests/test_zhipu.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1a9
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
6
|
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1a9"
|
|
4
4
|
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao..."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -30,6 +30,10 @@ logger = logging.getLogger(__name__)
|
|
|
30
30
|
# 注册 Auth 子应用
|
|
31
31
|
app.add_typer(auth_app, name="auth")
|
|
32
32
|
|
|
33
|
+
# 注册 Session 子应用
|
|
34
|
+
session_app = typer.Typer(name="session", help="管理 Session-Vendor 运行时绑定")
|
|
35
|
+
app.add_typer(session_app, name="session")
|
|
36
|
+
|
|
33
37
|
|
|
34
38
|
def _build_token_store(cfg_path: Path | None = None):
|
|
35
39
|
"""按配置解析 Token Store 路径并完成加载."""
|
|
@@ -264,6 +268,97 @@ def reset(
|
|
|
264
268
|
console.print("[red]代理服务未运行[/red]")
|
|
265
269
|
|
|
266
270
|
|
|
271
|
+
# ── Session 子命令 ───────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@session_app.command("bind")
|
|
275
|
+
def session_bind(
|
|
276
|
+
key: str = typer.Option(..., "--key", "-k", help="Session key"),
|
|
277
|
+
vendor: str = typer.Option(
|
|
278
|
+
..., "--vendor", "-v", help="绑定 vendor(逗号分隔多个)"
|
|
279
|
+
),
|
|
280
|
+
port: int = typer.Option(3392, "--port", "-p", help="代理服务端口"),
|
|
281
|
+
) -> None:
|
|
282
|
+
"""为指定 Session 绑定 vendor 优先级."""
|
|
283
|
+
import httpx as _httpx
|
|
284
|
+
|
|
285
|
+
vendors = [v.strip() for v in vendor.split(",") if v.strip()]
|
|
286
|
+
try:
|
|
287
|
+
resp = _httpx.put(
|
|
288
|
+
f"http://127.0.0.1:{port}/api/session-vendor",
|
|
289
|
+
json={"session_key": key, "vendors": vendors},
|
|
290
|
+
timeout=5,
|
|
291
|
+
)
|
|
292
|
+
if resp.status_code == 200:
|
|
293
|
+
data = resp.json()
|
|
294
|
+
console.print(
|
|
295
|
+
f"[green]绑定成功:[/] session [cyan]{key[:16]}…[/cyan] → "
|
|
296
|
+
+ " → ".join(data.get("vendors", vendors))
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
try:
|
|
300
|
+
err = resp.json()
|
|
301
|
+
msg = err.get("error", {}).get("message", resp.text)
|
|
302
|
+
except Exception:
|
|
303
|
+
msg = resp.text
|
|
304
|
+
console.print(f"[red]绑定失败: {msg}[/red]")
|
|
305
|
+
except _httpx.ConnectError:
|
|
306
|
+
console.print("[red]代理服务未运行[/red]")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@session_app.command("unbind")
|
|
310
|
+
def session_unbind(
|
|
311
|
+
key: str = typer.Option(..., "--key", "-k", help="Session key"),
|
|
312
|
+
port: int = typer.Option(3392, "--port", "-p", help="代理服务端口"),
|
|
313
|
+
) -> None:
|
|
314
|
+
"""解除指定 Session 的 vendor 绑定."""
|
|
315
|
+
from urllib.parse import quote
|
|
316
|
+
|
|
317
|
+
import httpx as _httpx
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
resp = _httpx.delete(
|
|
321
|
+
f"http://127.0.0.1:{port}/api/session-vendor/{quote(key, safe='')}",
|
|
322
|
+
timeout=5,
|
|
323
|
+
)
|
|
324
|
+
if resp.status_code == 200:
|
|
325
|
+
console.print(f"[green]已解除绑定:[/] session [cyan]{key[:16]}…[/cyan]")
|
|
326
|
+
elif resp.status_code == 404:
|
|
327
|
+
console.print(f"[yellow]未找到绑定:[/] session [cyan]{key[:16]}…[/cyan]")
|
|
328
|
+
else:
|
|
329
|
+
console.print(f"[red]解除失败: {resp.status_code} {resp.text}[/red]")
|
|
330
|
+
except _httpx.ConnectError:
|
|
331
|
+
console.print("[red]代理服务未运行[/red]")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@session_app.command("list")
|
|
335
|
+
def session_list(
|
|
336
|
+
port: int = typer.Option(3392, "--port", "-p", help="代理服务端口"),
|
|
337
|
+
) -> None:
|
|
338
|
+
"""列出所有运行时 Session-Vendor 绑定."""
|
|
339
|
+
import httpx as _httpx
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
resp = _httpx.get(
|
|
343
|
+
f"http://127.0.0.1:{port}/api/session-vendor",
|
|
344
|
+
timeout=5,
|
|
345
|
+
)
|
|
346
|
+
if resp.status_code == 200:
|
|
347
|
+
data = resp.json()
|
|
348
|
+
bindings = data.get("bindings", [])
|
|
349
|
+
if not bindings:
|
|
350
|
+
console.print("[dim]当前无运行时绑定[/dim]")
|
|
351
|
+
return
|
|
352
|
+
for b in bindings:
|
|
353
|
+
key = b.get("session_key", "?")
|
|
354
|
+
vendors = b.get("vendors", [])
|
|
355
|
+
console.print(f" [cyan]{key[:24]}…[/cyan] → " + " → ".join(vendors))
|
|
356
|
+
else:
|
|
357
|
+
console.print(f"[red]查询失败: {resp.status_code} {resp.text}[/red]")
|
|
358
|
+
except _httpx.ConnectError:
|
|
359
|
+
console.print("[red]代理服务未运行[/red]")
|
|
360
|
+
|
|
361
|
+
|
|
267
362
|
def _resolve_config_path(config: str | Path | None = None) -> Path | None:
|
|
268
363
|
"""标准化配置路径输入."""
|
|
269
364
|
if config is None:
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Session Policy 解析引擎 — 根据 session_key + client_category 解析适用的路由策略."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from ..config.session_policy import SessionPolicy, SessionPolicyMatch
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionPolicyResolver:
|
|
14
|
+
"""根据 session_key + client_category 解析适用的 SessionPolicy.
|
|
15
|
+
|
|
16
|
+
设计要点:
|
|
17
|
+
- 启动时构建索引,运行时 O(1) 查找
|
|
18
|
+
- 精确匹配优先:session_key > client_category > 无策略
|
|
19
|
+
- 无侵入性:不匹配时返回 None,路由行为与现有一致
|
|
20
|
+
- 运行时可变:支持 API 动态 upsert/remove session → vendor 绑定
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, policies: list[SessionPolicy] | None = None) -> None:
|
|
24
|
+
self._policies = policies or []
|
|
25
|
+
self._key_index: dict[str, SessionPolicy] = {}
|
|
26
|
+
self._category_index: dict[str, SessionPolicy] = {}
|
|
27
|
+
self._config_key_backup: dict[str, SessionPolicy] = {}
|
|
28
|
+
self._lock = threading.Lock()
|
|
29
|
+
self._build_index()
|
|
30
|
+
|
|
31
|
+
def _build_index(self) -> None:
|
|
32
|
+
"""构建 session_key / client_category → SessionPolicy 的查找索引.
|
|
33
|
+
|
|
34
|
+
按定义顺序遍历,首次出现的 key/category 获得最高优先级。
|
|
35
|
+
"""
|
|
36
|
+
for policy in self._policies:
|
|
37
|
+
for key in policy.match.session_keys:
|
|
38
|
+
if key not in self._key_index:
|
|
39
|
+
self._key_index[key] = policy
|
|
40
|
+
if (
|
|
41
|
+
policy.match.client_category
|
|
42
|
+
and policy.match.client_category not in self._category_index
|
|
43
|
+
):
|
|
44
|
+
self._category_index[policy.match.client_category] = policy
|
|
45
|
+
|
|
46
|
+
if self._key_index or self._category_index:
|
|
47
|
+
logger.info(
|
|
48
|
+
"SessionPolicyResolver initialized: %d key rules, %d category rules",
|
|
49
|
+
len(self._key_index),
|
|
50
|
+
len(self._category_index),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def resolve(
|
|
54
|
+
self, session_key: str, client_category: str = "cc"
|
|
55
|
+
) -> SessionPolicy | None:
|
|
56
|
+
"""返回匹配的策略,优先精确 session_key 匹配,其次 category 匹配.
|
|
57
|
+
|
|
58
|
+
返回的 SessionPolicy 对象应为不可变引用;调用方不应修改其内部属性,
|
|
59
|
+
否则在并发 upsert/remove 场景下可能产生竞态。
|
|
60
|
+
"""
|
|
61
|
+
with self._lock:
|
|
62
|
+
policy = self._key_index.get(session_key)
|
|
63
|
+
if policy:
|
|
64
|
+
return policy
|
|
65
|
+
return self._category_index.get(client_category)
|
|
66
|
+
|
|
67
|
+
# ── 运行时 session → vendor 绑定 ──────────────────────────────
|
|
68
|
+
|
|
69
|
+
def upsert(self, session_key: str, tier_names: list[str]) -> SessionPolicy:
|
|
70
|
+
"""为指定 session key 创建或替换运行时 vendor 绑定.
|
|
71
|
+
|
|
72
|
+
运行时策略使用 ``runtime:`` 名称前缀,与配置文件驱动的策略区分。
|
|
73
|
+
"""
|
|
74
|
+
policy = SessionPolicy(
|
|
75
|
+
name=f"runtime:{session_key}",
|
|
76
|
+
match=SessionPolicyMatch(session_keys=[session_key]),
|
|
77
|
+
tiers=tier_names,
|
|
78
|
+
)
|
|
79
|
+
with self._lock:
|
|
80
|
+
existing = self._key_index.get(session_key)
|
|
81
|
+
if existing and not existing.name.startswith("runtime:"):
|
|
82
|
+
self._config_key_backup[session_key] = existing
|
|
83
|
+
self._key_index[session_key] = policy
|
|
84
|
+
logger.info(
|
|
85
|
+
"Session vendor binding upserted: session_key=%s → %s",
|
|
86
|
+
session_key,
|
|
87
|
+
tier_names,
|
|
88
|
+
)
|
|
89
|
+
return policy
|
|
90
|
+
|
|
91
|
+
def remove(self, session_key: str) -> bool:
|
|
92
|
+
"""删除指定 session key 的运行时 vendor 绑定.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True 如果找到并删除了绑定,False 如果不存在。
|
|
96
|
+
"""
|
|
97
|
+
with self._lock:
|
|
98
|
+
policy = self._key_index.get(session_key)
|
|
99
|
+
if policy is None or not policy.name.startswith("runtime:"):
|
|
100
|
+
return False
|
|
101
|
+
del self._key_index[session_key]
|
|
102
|
+
# 恢复被运行时绑定覆盖的配置策略
|
|
103
|
+
backup = self._config_key_backup.pop(session_key, None)
|
|
104
|
+
if backup is not None:
|
|
105
|
+
self._key_index[session_key] = backup
|
|
106
|
+
logger.info("Session vendor binding removed: session_key=%s", session_key)
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def list_runtime_bindings(self) -> list[dict[str, str | list[str]]]:
|
|
110
|
+
"""返回所有运行时注入的绑定快照(仅 API 创建的,不含配置文件驱动的)."""
|
|
111
|
+
with self._lock:
|
|
112
|
+
return [
|
|
113
|
+
{"session_key": key, "vendors": policy.tiers}
|
|
114
|
+
for key, policy in self._key_index.items()
|
|
115
|
+
if policy.name.startswith("runtime:")
|
|
116
|
+
]
|
|
@@ -417,6 +417,19 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
417
417
|
}
|
|
418
418
|
.success-bar { width: 56px; height: 4px; border-radius: 2px; background: rgba(255,255,255,.06); display: inline-block; vertical-align: middle; margin-left: 6px; }
|
|
419
419
|
.success-bar-fill { height: 100%; border-radius: 2px; }
|
|
420
|
+
/* ── Vendor Bind 选择器 ── */
|
|
421
|
+
.bind-select {
|
|
422
|
+
padding: 3px 6px; border-radius: 6px;
|
|
423
|
+
background: rgba(48,54,61,.6); border: 1px solid rgba(255,255,255,.1);
|
|
424
|
+
color: var(--text-secondary); font-size: 12px;
|
|
425
|
+
font-family: 'JetBrains Mono', monospace;
|
|
426
|
+
cursor: pointer; outline: none;
|
|
427
|
+
transition: all .2s ease;
|
|
428
|
+
max-width: 120px;
|
|
429
|
+
}
|
|
430
|
+
.bind-select:hover { border-color: rgba(88,166,255,.4); color: var(--text-primary); }
|
|
431
|
+
.bind-select:focus { border-color: rgba(88,166,255,.6); box-shadow: 0 0 0 2px rgba(88,166,255,.1); }
|
|
432
|
+
.bind-select option { background: var(--bg-card); color: var(--text-primary); }
|
|
420
433
|
/* ── 加载态 ── */
|
|
421
434
|
.loading { opacity: .4; pointer-events: none; }
|
|
422
435
|
/* ── 图表标签截断 ── */
|
|
@@ -457,6 +470,34 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
457
470
|
margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border-subtle);
|
|
458
471
|
font-weight: 500; font-size: 12px; color: var(--text-secondary);
|
|
459
472
|
}
|
|
473
|
+
/* ── Tabs ─────────────────────────────────────────────────── */
|
|
474
|
+
.tabs {
|
|
475
|
+
display: flex;
|
|
476
|
+
gap: 4px;
|
|
477
|
+
margin-bottom: 16px;
|
|
478
|
+
border-bottom: 1px solid var(--border);
|
|
479
|
+
padding: 0 2px;
|
|
480
|
+
}
|
|
481
|
+
.tab-btn {
|
|
482
|
+
appearance: none;
|
|
483
|
+
background: transparent;
|
|
484
|
+
border: none;
|
|
485
|
+
border-bottom: 2px solid transparent;
|
|
486
|
+
color: var(--text-secondary);
|
|
487
|
+
cursor: pointer;
|
|
488
|
+
font-family: inherit;
|
|
489
|
+
font-size: 14px;
|
|
490
|
+
font-weight: 500;
|
|
491
|
+
padding: 10px 16px;
|
|
492
|
+
margin-bottom: -1px;
|
|
493
|
+
transition: color .15s ease, border-color .15s ease, background .15s ease;
|
|
494
|
+
border-radius: 6px 6px 0 0;
|
|
495
|
+
}
|
|
496
|
+
.tab-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); }
|
|
497
|
+
.tab-btn.active { color: var(--text-primary); border-bottom-color: var(--accent-blue); }
|
|
498
|
+
.tab-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
|
|
499
|
+
.tab-pane { display: none; }
|
|
500
|
+
.tab-pane.active { display: block; }
|
|
460
501
|
</style>
|
|
461
502
|
</head>
|
|
462
503
|
<body>
|
|
@@ -473,6 +514,14 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
473
514
|
</header>
|
|
474
515
|
|
|
475
516
|
<main>
|
|
517
|
+
<!-- 页签导航 -->
|
|
518
|
+
<nav class="tabs" role="tablist" aria-label="Dashboard sections">
|
|
519
|
+
<button type="button" class="tab-btn active" id="tab-btn-overview" role="tab" aria-controls="tab-pane-overview" aria-selected="true" data-tab="overview" onclick="switchTab('overview')">Overview</button>
|
|
520
|
+
<button type="button" class="tab-btn" id="tab-btn-sessions" role="tab" aria-controls="tab-pane-sessions" aria-selected="false" data-tab="sessions" onclick="switchTab('sessions')">Recent Active Sessions</button>
|
|
521
|
+
</nav>
|
|
522
|
+
|
|
523
|
+
<!-- Overview 页签 -->
|
|
524
|
+
<section class="tab-pane active" id="tab-pane-overview" role="tabpanel" aria-labelledby="tab-btn-overview" data-tab="overview">
|
|
476
525
|
<!-- 时间区间选择器 -->
|
|
477
526
|
<div class="time-range-bar">
|
|
478
527
|
<span class="time-range-label">时间区间</span>
|
|
@@ -562,7 +611,10 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
562
611
|
<div class="html-legend-wrap" id="model-token-legend" style="display:none"></div>
|
|
563
612
|
</div>
|
|
564
613
|
</div>
|
|
614
|
+
</section>
|
|
565
615
|
|
|
616
|
+
<!-- Recent Active Sessions 页签 -->
|
|
617
|
+
<section class="tab-pane" id="tab-pane-sessions" role="tabpanel" aria-labelledby="tab-btn-sessions" data-tab="sessions">
|
|
566
618
|
<!-- Recent Active Sessions -->
|
|
567
619
|
<div class="card sessions-card">
|
|
568
620
|
<div class="card-title">
|
|
@@ -581,15 +633,17 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
581
633
|
<th>Vendors</th>
|
|
582
634
|
<th>Avg Latency</th>
|
|
583
635
|
<th>Success</th>
|
|
636
|
+
<th>Vendor Bind</th>
|
|
584
637
|
<th>Client</th>
|
|
585
638
|
</tr>
|
|
586
639
|
</thead>
|
|
587
640
|
<tbody id="sessions-tbody">
|
|
588
|
-
<tr><td colspan="
|
|
641
|
+
<tr><td colspan="10" class="empty">Loading...</td></tr>
|
|
589
642
|
</tbody>
|
|
590
643
|
</table>
|
|
591
644
|
</div>
|
|
592
645
|
</div>
|
|
646
|
+
</section>
|
|
593
647
|
|
|
594
648
|
</main>
|
|
595
649
|
|
|
@@ -1363,16 +1417,31 @@ function formatVendorTags(vendors) {
|
|
|
1363
1417
|
}
|
|
1364
1418
|
async function updateSessions() {
|
|
1365
1419
|
try {
|
|
1366
|
-
var
|
|
1420
|
+
var results = await Promise.allSettled([
|
|
1421
|
+
fetchJSON('/api/dashboard/sessions?hours=24&limit=20'),
|
|
1422
|
+
fetchJSON('/api/session-vendor'),
|
|
1423
|
+
fetchJSON('/api/status'),
|
|
1424
|
+
]);
|
|
1425
|
+
if (results[0].status === 'rejected') throw results[0].reason;
|
|
1426
|
+
var data = results[0].value;
|
|
1427
|
+
var bindData = results[1].status === 'fulfilled' ? results[1].value : {bindings: []};
|
|
1428
|
+
var statusData = results[2].status === 'fulfilled' ? results[2].value : {tiers: []};
|
|
1367
1429
|
var sessions = data.sessions || [];
|
|
1430
|
+
var bindings = bindData.bindings || [];
|
|
1431
|
+
var availableVendors = (statusData.tiers || []).map(function(t) { return t.name; });
|
|
1368
1432
|
var tbody = document.getElementById('sessions-tbody');
|
|
1369
1433
|
var subtitle = document.getElementById('sessions-subtitle');
|
|
1370
1434
|
if (subtitle) subtitle.textContent = 'Last ' + data.hours + 'h';
|
|
1371
1435
|
if (!sessions.length) {
|
|
1372
|
-
tbody.innerHTML = '<tr><td colspan="
|
|
1436
|
+
tbody.innerHTML = '<tr><td colspan="10" class="empty"><div class="empty-icon">📭</div>No session data</td></tr>';
|
|
1373
1437
|
return;
|
|
1374
1438
|
}
|
|
1439
|
+
// Build binding lookup: session_key → vendors list
|
|
1440
|
+
var bindMap = {};
|
|
1441
|
+
bindings.forEach(function(b) { bindMap[b.session_key] = b.vendors; });
|
|
1375
1442
|
tbody.innerHTML = sessions.map(function(s) {
|
|
1443
|
+
var boundVendors = bindMap[s.session_key];
|
|
1444
|
+
var selectHtml = buildBindSelect(s.session_key, boundVendors, availableVendors);
|
|
1376
1445
|
return '<tr>' +
|
|
1377
1446
|
'<td class="session-key" title="' + escapeHtml(s.session_key) + '">' + truncateKey(s.session_key, 22) + '</td>' +
|
|
1378
1447
|
'<td>' + relativeTime(s.last_active_ts) + '</td>' +
|
|
@@ -1382,6 +1451,7 @@ async function updateSessions() {
|
|
|
1382
1451
|
'<td>' + formatVendorTags(s.vendors) + '</td>' +
|
|
1383
1452
|
'<td style="font-family:JetBrains Mono,monospace">' + (s.avg_duration_ms ? Math.round(s.avg_duration_ms) + 'ms' : '–') + '</td>' +
|
|
1384
1453
|
'<td>' + successBarHtml(s.success_rate) + '</td>' +
|
|
1454
|
+
'<td>' + selectHtml + '</td>' +
|
|
1385
1455
|
'<td>' + formatCategories(s.client_categories) + '</td>' +
|
|
1386
1456
|
'</tr>';
|
|
1387
1457
|
}).join('');
|
|
@@ -1390,48 +1460,170 @@ async function updateSessions() {
|
|
|
1390
1460
|
}
|
|
1391
1461
|
}
|
|
1392
1462
|
|
|
1393
|
-
|
|
1463
|
+
function buildBindSelect(sessionKey, boundVendors, availableVendors) {
|
|
1464
|
+
var isBound = boundVendors && boundVendors.length > 0;
|
|
1465
|
+
var multiBound = isBound && boundVendors.length > 1;
|
|
1466
|
+
var selected = isBound ? boundVendors[0] : '';
|
|
1467
|
+
var html = '<select class="bind-select" data-session-key="' + escapeHtml(sessionKey) + '">';
|
|
1468
|
+
html += '<option value=""' + (!isBound ? ' selected' : '') + '>Default</option>';
|
|
1469
|
+
availableVendors.forEach(function(v) {
|
|
1470
|
+
var label = multiBound && v === selected ? escapeHtml(v) + ' (+' + (boundVendors.length - 1) + ')' : escapeHtml(v);
|
|
1471
|
+
html += '<option value="' + escapeHtml(v) + '"' + (v === selected ? ' selected' : '') + '>' + label + '</option>';
|
|
1472
|
+
});
|
|
1473
|
+
html += '</select>';
|
|
1474
|
+
return html;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async function handleBindChange(sel) {
|
|
1478
|
+
var sessionKey = sel.getAttribute('data-session-key');
|
|
1479
|
+
var vendor = sel.value;
|
|
1480
|
+
var previousValue = sel.getAttribute('data-previous') || '';
|
|
1481
|
+
try {
|
|
1482
|
+
var resp;
|
|
1483
|
+
if (vendor) {
|
|
1484
|
+
resp = await fetch('/api/session-vendor', {
|
|
1485
|
+
method: 'PUT',
|
|
1486
|
+
headers: {'Content-Type': 'application/json'},
|
|
1487
|
+
body: JSON.stringify({session_key: sessionKey, vendors: [vendor]}),
|
|
1488
|
+
});
|
|
1489
|
+
} else {
|
|
1490
|
+
resp = await fetch('/api/session-vendor/' + encodeURIComponent(sessionKey), {method: 'DELETE'});
|
|
1491
|
+
}
|
|
1492
|
+
if (!resp.ok) {
|
|
1493
|
+
sel.value = previousValue;
|
|
1494
|
+
console.error('Bind change rejected:', resp.status, await resp.text());
|
|
1495
|
+
}
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
sel.value = previousValue;
|
|
1498
|
+
console.error('Bind change failed:', e);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
var sessionsTbody = document.getElementById('sessions-tbody');
|
|
1503
|
+
sessionsTbody.addEventListener('focus', function(e) {
|
|
1504
|
+
if (e.target.classList.contains('bind-select')) {
|
|
1505
|
+
e.target.setAttribute('data-previous', e.target.value);
|
|
1506
|
+
}
|
|
1507
|
+
}, true);
|
|
1508
|
+
sessionsTbody.addEventListener('change', function(e) {
|
|
1509
|
+
if (e.target.classList.contains('bind-select')) {
|
|
1510
|
+
handleBindChange(e.target);
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// ── 主刷新逻辑(按 Tab 分发) ──────────────────────────────
|
|
1394
1515
|
let refreshing = false;
|
|
1516
|
+
let currentTab = 'overview';
|
|
1517
|
+
const tabLoaded = { overview: false, sessions: false };
|
|
1518
|
+
const TAB_LABELS = { overview: 'Overview', sessions: 'Recent Active Sessions' };
|
|
1519
|
+
|
|
1520
|
+
async function refreshOverview() {
|
|
1521
|
+
const days = currentDays > 0 ? currentDays : 7;
|
|
1522
|
+
const [summary, timeline, status] = await Promise.all([
|
|
1523
|
+
fetchJSON('/api/dashboard/summary?days=' + days),
|
|
1524
|
+
fetchJSON('/api/dashboard/timeline?days=' + days),
|
|
1525
|
+
fetchJSON('/api/status'),
|
|
1526
|
+
]);
|
|
1527
|
+
|
|
1528
|
+
if (summary.version) {
|
|
1529
|
+
document.getElementById('version-badge').textContent = 'v' + summary.version;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
updateKPI(summary);
|
|
1533
|
+
updateVendorStatus(status);
|
|
1534
|
+
updateChartTitles(days);
|
|
1535
|
+
|
|
1536
|
+
const rows = timeline.rows || [];
|
|
1537
|
+
const tierOrder = (status.tiers || []).map(t => t.name);
|
|
1538
|
+
buildTimeline(rows, tierOrder);
|
|
1539
|
+
buildVendorDist(rows, tierOrder);
|
|
1540
|
+
buildTokenTimeline(rows, tierOrder);
|
|
1541
|
+
buildModelTokenTimeline(rows);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async function refreshSessions() {
|
|
1545
|
+
await updateSessions();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1395
1548
|
async function refresh() {
|
|
1396
1549
|
if (refreshing) return;
|
|
1397
1550
|
refreshing = true;
|
|
1398
|
-
document.getElementById('refresh-time').textContent = '刷新中…';
|
|
1399
1551
|
try {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1552
|
+
// 循环:若 await 期间用户切到了尚未加载的另一页签,补一次刷新,避免 tabLoaded 错位。
|
|
1553
|
+
while (true) {
|
|
1554
|
+
const tab = currentTab;
|
|
1555
|
+
document.getElementById('refresh-time').textContent = '刷新中…';
|
|
1556
|
+
try {
|
|
1557
|
+
if (tab === 'sessions') {
|
|
1558
|
+
await refreshSessions();
|
|
1559
|
+
} else {
|
|
1560
|
+
await refreshOverview();
|
|
1561
|
+
}
|
|
1562
|
+
tabLoaded[tab] = true;
|
|
1563
|
+
if (tab === currentTab) {
|
|
1564
|
+
document.getElementById('refresh-time').textContent =
|
|
1565
|
+
'上次刷新: ' + now() + '(' + TAB_LABELS[tab] + ')';
|
|
1566
|
+
}
|
|
1567
|
+
} catch (e) {
|
|
1568
|
+
console.error('Dashboard refresh error:', e);
|
|
1569
|
+
document.getElementById('refresh-time').textContent = '刷新失败 ' + now();
|
|
1570
|
+
}
|
|
1571
|
+
if (currentTab !== tab && !tabLoaded[currentTab]) continue;
|
|
1572
|
+
break;
|
|
1409
1573
|
}
|
|
1574
|
+
} finally {
|
|
1575
|
+
refreshing = false;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1410
1578
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1579
|
+
// ── 页签切换(懒加载 + URL 同步) ─────────────────────────
|
|
1580
|
+
function syncTabUrl(name) {
|
|
1581
|
+
try {
|
|
1582
|
+
const url = new URL(window.location.href);
|
|
1583
|
+
if (url.searchParams.get('tab') === name) return;
|
|
1584
|
+
url.searchParams.set('tab', name);
|
|
1585
|
+
window.history.replaceState({}, '', url);
|
|
1586
|
+
} catch (e) { /* no-op */ }
|
|
1587
|
+
}
|
|
1414
1588
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1589
|
+
function applyTabState(name) {
|
|
1590
|
+
document.querySelectorAll('.tab-btn').forEach(function (b) {
|
|
1591
|
+
const active = b.getAttribute('data-tab') === name;
|
|
1592
|
+
b.classList.toggle('active', active);
|
|
1593
|
+
b.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
1594
|
+
});
|
|
1595
|
+
document.querySelectorAll('.tab-pane').forEach(function (p) {
|
|
1596
|
+
p.classList.toggle('active', p.getAttribute('data-tab') === name);
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1422
1599
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1600
|
+
function switchTab(name) {
|
|
1601
|
+
if (name !== 'overview' && name !== 'sessions') name = 'overview';
|
|
1602
|
+
if (name === currentTab) {
|
|
1603
|
+
syncTabUrl(name);
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
currentTab = name;
|
|
1607
|
+
applyTabState(name);
|
|
1608
|
+
syncTabUrl(name);
|
|
1609
|
+
if (!tabLoaded[name]) {
|
|
1610
|
+
refresh();
|
|
1429
1611
|
}
|
|
1430
1612
|
}
|
|
1431
1613
|
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1434
|
-
|
|
1614
|
+
// ── 初始化 ────────────────────────────────────────────────
|
|
1615
|
+
(function bootstrap() {
|
|
1616
|
+
let initial = 'overview';
|
|
1617
|
+
try {
|
|
1618
|
+
const t = new URL(window.location.href).searchParams.get('tab');
|
|
1619
|
+
if (t === 'sessions') initial = 'sessions';
|
|
1620
|
+
} catch (e) { /* no-op */ }
|
|
1621
|
+
currentTab = initial;
|
|
1622
|
+
applyTabState(initial);
|
|
1623
|
+
syncTabUrl(initial);
|
|
1624
|
+
refresh(); // 仅加载初始页签的数据
|
|
1625
|
+
setInterval(refresh, 600000); // 每 10 分钟刷新当前页签
|
|
1626
|
+
})();
|
|
1435
1627
|
</script>
|
|
1436
1628
|
</body>
|
|
1437
1629
|
</html>
|