coding-proxy 0.3.1a9__tar.gz → 0.4.0__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.1a9 → coding_proxy-0.4.0}/.gitignore +3 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/CHANGELOG.md +19 -8
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/PKG-INFO +1 -1
- coding_proxy-0.4.0/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/pyproject.toml +1 -1
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/dashboard.py +252 -74
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/uv.lock +1 -1
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/AGENTS.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/CLAUDE.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/LICENSE +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/README.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/assets/dashboard-v0.2.4.png +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/convert.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/routing.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/testing.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/ci-cd.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/framework.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/issue.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/user-guide.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/__init__.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_banner.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_compat.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_config_init.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_currency.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_mixins.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_token.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_pricing.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_router_executor.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_schema.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_tier.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_time_range.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_types.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_vendors.py +0 -0
- {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_zhipu.py +0 -0
|
@@ -4,16 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- fix(vendor-channels): 新增 zhipu 同 vendor 自清理通道,修复 GLM-5 自循环 400 + tool_results 偶发降级;
|
|
9
|
-
- fix(vendor-channels): 修复 `_rewrite_srvtoolu_ids` 块顺序敏感性导致 inline tool_result 漏改名,进而 enforce 阶段 dict key 与 tool_use_ids 错位、anthropic 报 `tool_use ids without tool_result blocks immediately after` 的 cascade failover 问题(改为两遍扫描:先收集 id_map,再统一改写所有 tool_result.tool_use_id 引用);
|
|
10
|
-
- fix(vendor-channels): `enforce_anthropic_tool_pairing` 增加全局 sanity check pass,主循环边角错位让 dangling tool_use 漏过校验时兜底合成 is_error 占位并打 `pairing_sanity_repaired` 标签,避免 anthropic 二次报错;
|
|
7
|
+
## [v0.4.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.4.0) — 2026-05-01
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
>
|
|
11
|
+
> **🚀 Session 级专属路由策略!**
|
|
12
|
+
>
|
|
13
|
+
> 给每个 Session 指定专属的 vendor,动态调节不同 vendors 间的 LLM 流量。
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### ✨ 核心亮点
|
|
18
|
+
|
|
19
|
+
- feat(session-policy): 新增 Session 级专属路由策略 (#219)
|
|
20
|
+
- feat(dashboard): 新增会话活动面板 (#222)
|
|
21
|
+
|
|
22
|
+
### 🔧 更多特性
|
|
13
23
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- fix(
|
|
24
|
+
- refactor(logging): 移除已被 ModelCall 汇总行覆盖的冗余 DEBUG 日志 (#203)
|
|
25
|
+
- style(dashboard): 加宽图表 tooltip 令模型名称与用量值单行显示 (#211)
|
|
26
|
+
- fix(usage-parser): 补充 OpenAI/Gemini SSE 流式分支的 model_served 提取 (#214)
|
|
27
|
+
- fix(usage-parser): 兼容 SSE chunk 中 usage 字段为 null 的极端格式 (#212)
|
|
17
28
|
|
|
18
29
|
## [v0.3.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.3.0) — 2026-04-20
|
|
19
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
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"
|
|
@@ -159,7 +159,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
159
159
|
.kpi-grid {
|
|
160
160
|
display: grid;
|
|
161
161
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
162
|
-
gap:
|
|
162
|
+
gap: 5px;
|
|
163
163
|
margin-bottom: 24px;
|
|
164
164
|
}
|
|
165
165
|
.kpi-card {
|
|
@@ -310,6 +310,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
310
310
|
}
|
|
311
311
|
.vendor-name { font-weight: 600; font-size: 14px; }
|
|
312
312
|
.vendor-badges { display: flex; gap: 5px; flex-wrap: wrap; align-items: center; }
|
|
313
|
+
.quota-group { display: flex; align-items: center; gap: 6px; }
|
|
313
314
|
.status-badge {
|
|
314
315
|
font-size: 11px; padding: 2px 7px;
|
|
315
316
|
border-radius: 10px;
|
|
@@ -319,7 +320,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
319
320
|
.sb-warn { background: rgba(210,153,34,.12); color: var(--accent-yellow); border: 1px solid rgba(210,153,34,.2); }
|
|
320
321
|
.sb-err { background: rgba(248,81,73,.12); color: var(--accent-red); border: 1px solid rgba(248,81,73,.2); }
|
|
321
322
|
.sb-info { background: rgba(88,166,255,.12); color: var(--accent-blue); border: 1px solid rgba(88,166,255,.2); }
|
|
322
|
-
.quota-bar-wrap { flex: 1;
|
|
323
|
+
.quota-bar-wrap { flex: 1; min-width: 40px; max-width: 100px; }
|
|
323
324
|
.quota-bar-bg {
|
|
324
325
|
height: 4px; border-radius: 2px;
|
|
325
326
|
background: rgba(255,255,255,.06);
|
|
@@ -397,8 +398,8 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
397
398
|
.empty-icon { font-size: 32px; margin-bottom: 8px; opacity: .5; }
|
|
398
399
|
/* ── Sessions Panel ── */
|
|
399
400
|
.sessions-card { grid-column: 1 / -1; animation-delay: .1s; }
|
|
400
|
-
.session-table-wrap { overflow
|
|
401
|
-
.session-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
401
|
+
.session-table-wrap { overflow: hidden; }
|
|
402
|
+
.session-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
|
|
402
403
|
.session-table th {
|
|
403
404
|
position: sticky; top: 0; z-index: 1;
|
|
404
405
|
background: var(--bg-card); padding: 10px 12px;
|
|
@@ -406,17 +407,59 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
406
407
|
color: var(--text-secondary); text-transform: uppercase; letter-spacing: .5px;
|
|
407
408
|
border-bottom: 1px solid var(--border);
|
|
408
409
|
}
|
|
409
|
-
.session-table td { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; }
|
|
410
|
+
.session-table td { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
411
|
+
.session-table td.cell-tags { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.8; vertical-align: middle; }
|
|
410
412
|
.session-table tr:hover td { background: var(--bg-card-hover); }
|
|
411
|
-
.session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; }
|
|
413
|
+
.session-table .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
414
|
+
.session-id { display: flex; align-items: center; gap: 4px; }
|
|
415
|
+
.session-id-text { overflow: hidden; text-overflow: ellipsis; }
|
|
416
|
+
.copy-btn { background: none; border: none; color: var(--text-tertiary); cursor: pointer; padding: 2px; border-radius: 4px; font-size: 12px; line-height: 1; opacity: .5; flex-shrink: 0; }
|
|
417
|
+
.copy-btn:hover { opacity: 1; color: var(--accent-blue); background: rgba(88,166,255,.1); }
|
|
418
|
+
.copy-btn.copied { color: var(--accent-green); opacity: 1; }
|
|
419
|
+
.session-meta { font-size: 10px; color: var(--text-tertiary); line-height: 1.2; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
412
420
|
.session-tag {
|
|
413
421
|
display: inline-block; font-size: 11px; padding: 2px 7px;
|
|
414
422
|
border-radius: 8px; margin: 1px 2px;
|
|
415
423
|
background: rgba(88,166,255,.08); border: 1px solid rgba(88,166,255,.15);
|
|
416
424
|
color: var(--text-secondary);
|
|
417
425
|
}
|
|
418
|
-
.
|
|
419
|
-
|
|
426
|
+
.session-tag-cc {
|
|
427
|
+
background: rgba(63,185,80,.08); border-color: rgba(63,185,80,.15);
|
|
428
|
+
}
|
|
429
|
+
.session-table td.cell-success { overflow: visible; text-overflow: clip; }
|
|
430
|
+
/* ── 展开行 ── */
|
|
431
|
+
.session-table tr.row-detail { display: none; }
|
|
432
|
+
.session-table tr.row-detail.open { display: table-row; }
|
|
433
|
+
.session-table tr.row-detail td { padding: 0; }
|
|
434
|
+
.detail-card {
|
|
435
|
+
padding: 16px 24px; margin: 6px 0;
|
|
436
|
+
background: linear-gradient(135deg, rgba(30,37,54,.95), rgba(22,28,40,.95));
|
|
437
|
+
border: 1px solid rgba(88,166,255,.15); border-radius: 12px;
|
|
438
|
+
font-size: 13px;
|
|
439
|
+
white-space: normal; overflow: hidden;
|
|
440
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.3);
|
|
441
|
+
}
|
|
442
|
+
.detail-card .detail-item { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
443
|
+
.detail-card .detail-label { font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .3px; }
|
|
444
|
+
.detail-card .detail-value { color: var(--text-primary); line-height: 1.4; word-break: break-all; overflow-wrap: break-word; }
|
|
445
|
+
.detail-identity-row {
|
|
446
|
+
display: flex; gap: 16px;
|
|
447
|
+
padding-bottom: 10px; margin-bottom: 10px;
|
|
448
|
+
border-bottom: 1px solid var(--border);
|
|
449
|
+
}
|
|
450
|
+
.detail-identity-row .detail-item { flex: 3 1 0; }
|
|
451
|
+
.detail-identity-row .detail-item:first-child { flex: 2 1 0; }
|
|
452
|
+
.detail-identity-row .detail-value { font-family: 'JetBrains Mono', monospace; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; word-break: normal; }
|
|
453
|
+
.detail-metrics-grid {
|
|
454
|
+
display: grid;
|
|
455
|
+
grid-template-columns: repeat(8, 1fr);
|
|
456
|
+
gap: 10px 16px;
|
|
457
|
+
}
|
|
458
|
+
.detail-inline-pair { display: flex; gap: 16px; }
|
|
459
|
+
.detail-inline-pair > div { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
460
|
+
.session-table tbody tr[data-row]:not(.row-detail) { cursor: pointer; }
|
|
461
|
+
.success-bar { width: 56px; height: 4px; border-radius: 2px; background: rgba(255,255,255,.12); display: inline-block; vertical-align: middle; margin-left: 6px; }
|
|
462
|
+
.success-bar-fill { height: 100%; border-radius: 2px; display: block; }
|
|
420
463
|
/* ── Vendor Bind 选择器 ── */
|
|
421
464
|
.bind-select {
|
|
422
465
|
padding: 3px 6px; border-radius: 6px;
|
|
@@ -430,6 +473,21 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
430
473
|
.bind-select:hover { border-color: rgba(88,166,255,.4); color: var(--text-primary); }
|
|
431
474
|
.bind-select:focus { border-color: rgba(88,166,255,.6); box-shadow: 0 0 0 2px rgba(88,166,255,.1); }
|
|
432
475
|
.bind-select option { background: var(--bg-card); color: var(--text-primary); }
|
|
476
|
+
/* ── 分页 ── */
|
|
477
|
+
.session-pagination {
|
|
478
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
479
|
+
padding: 10px 12px; border-top: 1px solid var(--border-subtle);
|
|
480
|
+
font-size: 12px; color: var(--text-secondary);
|
|
481
|
+
}
|
|
482
|
+
.page-btn {
|
|
483
|
+
padding: 4px 10px; border-radius: 6px;
|
|
484
|
+
background: rgba(48,54,61,.4); border: 1px solid rgba(255,255,255,.08);
|
|
485
|
+
color: var(--text-secondary); font-size: 12px; cursor: pointer;
|
|
486
|
+
transition: all .15s ease;
|
|
487
|
+
}
|
|
488
|
+
.page-btn:hover:not(:disabled) { background: var(--bg-card-hover); color: var(--text-primary); border-color: rgba(88,166,255,.3); }
|
|
489
|
+
.page-btn:disabled { opacity: .35; cursor: default; }
|
|
490
|
+
.page-info { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
|
|
433
491
|
/* ── 加载态 ── */
|
|
434
492
|
.loading { opacity: .4; pointer-events: none; }
|
|
435
493
|
/* ── 图表标签截断 ── */
|
|
@@ -473,28 +531,28 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
473
531
|
/* ── Tabs ─────────────────────────────────────────────────── */
|
|
474
532
|
.tabs {
|
|
475
533
|
display: flex;
|
|
476
|
-
gap:
|
|
477
|
-
|
|
478
|
-
border-bottom: 1px solid var(--border);
|
|
479
|
-
padding: 0 2px;
|
|
534
|
+
gap: 2px;
|
|
535
|
+
padding: 0;
|
|
480
536
|
}
|
|
481
537
|
.tab-btn {
|
|
482
538
|
appearance: none;
|
|
483
539
|
background: transparent;
|
|
484
|
-
border:
|
|
485
|
-
border-bottom: 2px solid transparent;
|
|
540
|
+
border: 1px solid transparent;
|
|
486
541
|
color: var(--text-secondary);
|
|
487
542
|
cursor: pointer;
|
|
488
543
|
font-family: inherit;
|
|
489
|
-
font-size:
|
|
544
|
+
font-size: 13px;
|
|
490
545
|
font-weight: 500;
|
|
491
|
-
padding:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
border-radius: 6px 6px 0 0;
|
|
546
|
+
padding: 4px 12px;
|
|
547
|
+
transition: color .15s ease, background .15s ease, border-color .15s ease;
|
|
548
|
+
border-radius: var(--radius-sm);
|
|
495
549
|
}
|
|
496
550
|
.tab-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); }
|
|
497
|
-
.tab-btn.active {
|
|
551
|
+
.tab-btn.active {
|
|
552
|
+
color: var(--text-primary);
|
|
553
|
+
background: rgba(88,166,255,.1);
|
|
554
|
+
border-color: rgba(88,166,255,.2);
|
|
555
|
+
}
|
|
498
556
|
.tab-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
|
|
499
557
|
.tab-pane { display: none; }
|
|
500
558
|
.tab-pane.active { display: block; }
|
|
@@ -508,18 +566,16 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
508
566
|
<span class="badge" id="version-badge">v-.-.-</span>
|
|
509
567
|
</div>
|
|
510
568
|
<div class="header-right">
|
|
569
|
+
<nav class="tabs" role="tablist" aria-label="Dashboard sections">
|
|
570
|
+
<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>
|
|
571
|
+
<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')">Sessions</button>
|
|
572
|
+
</nav>
|
|
511
573
|
<span class="refresh-time" id="refresh-time">正在加载…</span>
|
|
512
574
|
<button class="btn-refresh" onclick="refresh()">⟳ 刷新</button>
|
|
513
575
|
</div>
|
|
514
576
|
</header>
|
|
515
577
|
|
|
516
578
|
<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
579
|
<!-- Overview 页签 -->
|
|
524
580
|
<section class="tab-pane active" id="tab-pane-overview" role="tabpanel" aria-labelledby="tab-btn-overview" data-tab="overview">
|
|
525
581
|
<!-- 时间区间选择器 -->
|
|
@@ -613,19 +669,27 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
613
669
|
</div>
|
|
614
670
|
</section>
|
|
615
671
|
|
|
616
|
-
<!--
|
|
672
|
+
<!-- Sessions 页签 -->
|
|
617
673
|
<section class="tab-pane" id="tab-pane-sessions" role="tabpanel" aria-labelledby="tab-btn-sessions" data-tab="sessions">
|
|
618
|
-
<!--
|
|
674
|
+
<!-- Sessions -->
|
|
619
675
|
<div class="card sessions-card">
|
|
620
|
-
<div class="card-title">
|
|
621
|
-
<span>Recent Active Sessions</span>
|
|
622
|
-
<span style="font-size:12px;color:var(--text-tertiary)" id="sessions-subtitle">Last 24h</span>
|
|
623
|
-
</div>
|
|
624
676
|
<div class="session-table-wrap" id="sessions-table-wrap">
|
|
625
677
|
<table class="session-table">
|
|
678
|
+
<colgroup>
|
|
679
|
+
<col style="width:12%">
|
|
680
|
+
<col style="width:7%">
|
|
681
|
+
<col style="width:6%">
|
|
682
|
+
<col style="width:6%">
|
|
683
|
+
<col style="width:17%">
|
|
684
|
+
<col style="width:12%">
|
|
685
|
+
<col style="width:7%">
|
|
686
|
+
<col style="width:9%">
|
|
687
|
+
<col style="width:12%">
|
|
688
|
+
<col style="width:12%">
|
|
689
|
+
</colgroup>
|
|
626
690
|
<thead>
|
|
627
691
|
<tr>
|
|
628
|
-
<th>Session</th>
|
|
692
|
+
<th>Session ID</th>
|
|
629
693
|
<th>Last Active</th>
|
|
630
694
|
<th>Requests</th>
|
|
631
695
|
<th>Tokens</th>
|
|
@@ -641,6 +705,14 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
|
|
|
641
705
|
<tr><td colspan="10" class="empty">Loading...</td></tr>
|
|
642
706
|
</tbody>
|
|
643
707
|
</table>
|
|
708
|
+
<div class="session-pagination" id="session-pagination">
|
|
709
|
+
<span class="page-info" id="page-info"></span>
|
|
710
|
+
<div style="display:flex;gap:6px;align-items:center">
|
|
711
|
+
<button class="page-btn" id="btn-prev" onclick="changePage(-1)">Prev</button>
|
|
712
|
+
<span class="page-info" id="page-num"></span>
|
|
713
|
+
<button class="page-btn" id="btn-next" onclick="changePage(1)">Next</button>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
644
716
|
</div>
|
|
645
717
|
</div>
|
|
646
718
|
</section>
|
|
@@ -684,7 +756,32 @@ function fmtTokens(n) {
|
|
|
684
756
|
return String(n);
|
|
685
757
|
}
|
|
686
758
|
function fmtNum(n) { return n == null ? '–' : n.toLocaleString(); }
|
|
759
|
+
function copyFromParent(btn) {
|
|
760
|
+
var text = btn.parentElement.getAttribute('data-key') || btn.parentElement.getAttribute('title') || '';
|
|
761
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
762
|
+
btn.classList.add('copied');
|
|
763
|
+
btn.textContent = '✓';
|
|
764
|
+
setTimeout(function() { btn.classList.remove('copied'); btn.textContent = '⧉'; }, 1500);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function toggleRow(tr) {
|
|
768
|
+
var detail = tr.nextElementSibling;
|
|
769
|
+
if (!detail || !detail.classList.contains('row-detail')) return;
|
|
770
|
+
var wasOpen = detail.classList.contains('open');
|
|
771
|
+
// close all open rows first
|
|
772
|
+
document.querySelectorAll('.session-table tr.row-detail.open').forEach(function(r) { r.classList.remove('open'); });
|
|
773
|
+
if (!wasOpen) detail.classList.add('open');
|
|
774
|
+
}
|
|
687
775
|
function isValidLabel(s) { return typeof s === 'string' && s !== 'undefined' && s !== 'null' && s.trim() !== ''; }
|
|
776
|
+
function fmtDuration(ms) {
|
|
777
|
+
if (ms == null) return '–';
|
|
778
|
+
var s = ms / 1000;
|
|
779
|
+
if (s < 1) return Math.round(ms) + 'ms';
|
|
780
|
+
if (s < 60) return s.toFixed(1).replace(/\\.0$/, '') + 's';
|
|
781
|
+
var m = Math.floor(s / 60);
|
|
782
|
+
var sec = Math.round(s % 60);
|
|
783
|
+
return sec > 0 ? m + 'min ' + sec + 's' : m + 'min';
|
|
784
|
+
}
|
|
688
785
|
function now() {
|
|
689
786
|
return new Date().toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
690
787
|
}
|
|
@@ -694,12 +791,15 @@ function now() {
|
|
|
694
791
|
// _API_VENDORS 需与后端 native_api/handler.py::_VENDOR_LABEL 对齐,
|
|
695
792
|
// 新增无 -native 后缀的 native vendor 时同步更新本集合。
|
|
696
793
|
const _API_VENDORS = new Set(['anthropic-native', 'openai', 'gemini']);
|
|
794
|
+
function isApiVendor(v) { return _API_VENDORS.has(v); }
|
|
795
|
+
function vendorShortName(v) {
|
|
796
|
+
if (!isValidLabel(v)) return v;
|
|
797
|
+
if (isApiVendor(v)) return v.endsWith('-native') ? v.slice(0, -'-native'.length) : v;
|
|
798
|
+
return v;
|
|
799
|
+
}
|
|
697
800
|
function formatVendorLabel(v) {
|
|
698
801
|
if (!isValidLabel(v)) return v;
|
|
699
|
-
if (
|
|
700
|
-
const name = v.endsWith('-native') ? v.slice(0, -'-native'.length) : v;
|
|
701
|
-
return 'api | ' + name;
|
|
702
|
-
}
|
|
802
|
+
if (isApiVendor(v)) return 'api | ' + vendorShortName(v);
|
|
703
803
|
return 'cc | ' + v;
|
|
704
804
|
}
|
|
705
805
|
|
|
@@ -990,10 +1090,11 @@ function renderQuotaBar(qg) {
|
|
|
990
1090
|
if (!qg || qg.usage_percent == null) return '';
|
|
991
1091
|
const pct = Math.round(qg.usage_percent);
|
|
992
1092
|
const label = quotaWindowLabel(qg.window_hours);
|
|
993
|
-
return `<
|
|
1093
|
+
return `<div class="quota-group">` +
|
|
1094
|
+
`<span class="status-badge ${quotaClass(pct)}">${label} ${pct}%</span>` +
|
|
994
1095
|
`<div class="quota-bar-wrap"><div class="quota-bar-bg">` +
|
|
995
1096
|
`<div class="quota-bar-fill" style="width:${Math.min(pct,100)}%;background:${quotaBarColor(pct)}"></div>` +
|
|
996
|
-
`</div></div>`;
|
|
1097
|
+
`</div></div></div>`;
|
|
997
1098
|
}
|
|
998
1099
|
|
|
999
1100
|
function updateVendorStatus(status) {
|
|
@@ -1385,6 +1486,11 @@ function truncateKey(key, maxLen) {
|
|
|
1385
1486
|
if (!key || key.length <= maxLen) return escapeHtml(key) || '–';
|
|
1386
1487
|
return escapeHtml(key.slice(0, maxLen - 3)) + '…';
|
|
1387
1488
|
}
|
|
1489
|
+
function parseSessionKey(raw) {
|
|
1490
|
+
try { var o = JSON.parse(raw); return { device_id: o.device_id||'', account_uuid: o.account_uuid||'', session_id: o.session_id||'' }; }
|
|
1491
|
+
catch(e) { return { device_id:'', account_uuid:'', session_id: raw || '' }; }
|
|
1492
|
+
}
|
|
1493
|
+
function shortId(s, n) { return s ? (s.length <= n ? s : s.slice(0, n) + '…') : ''; }
|
|
1388
1494
|
function successBarHtml(pct) {
|
|
1389
1495
|
if (pct == null) return '–';
|
|
1390
1496
|
var p = Math.round(pct);
|
|
@@ -1398,7 +1504,10 @@ function formatSessionTags(str, max) {
|
|
|
1398
1504
|
var html = list.slice(0, max).map(function(c) {
|
|
1399
1505
|
return '<span class="session-tag">' + escapeHtml(c.trim()) + '</span>';
|
|
1400
1506
|
}).join('');
|
|
1401
|
-
if (list.length > max)
|
|
1507
|
+
if (list.length > max) {
|
|
1508
|
+
var fullList = list.map(function(c) { return c.trim(); }).join(', ');
|
|
1509
|
+
html += '<span class="session-tag" title="' + escapeHtml(fullList) + '">+' + (list.length - max) + '</span>';
|
|
1510
|
+
}
|
|
1402
1511
|
return html;
|
|
1403
1512
|
}
|
|
1404
1513
|
function formatCategories(cats) {
|
|
@@ -1411,14 +1520,32 @@ function formatCategories(cats) {
|
|
|
1411
1520
|
}
|
|
1412
1521
|
function formatVendorTags(vendors) {
|
|
1413
1522
|
if (!vendors) return '–';
|
|
1414
|
-
|
|
1415
|
-
|
|
1523
|
+
var list = vendors.split(',');
|
|
1524
|
+
var max = 4;
|
|
1525
|
+
var html = list.slice(0, max).map(function(v) {
|
|
1526
|
+
var vt = v.trim();
|
|
1527
|
+
var name = vendorShortName(vt);
|
|
1528
|
+
var fullLabel = formatVendorLabel(vt);
|
|
1529
|
+
var cls = isApiVendor(vt) ? 'session-tag' : 'session-tag session-tag-cc';
|
|
1530
|
+
return '<span class="' + cls + '" title="' + escapeHtml(fullLabel) + '">' + escapeHtml(name) + '</span>';
|
|
1416
1531
|
}).join('');
|
|
1532
|
+
if (list.length > max) {
|
|
1533
|
+
var fullList = list.map(function(v) { return formatVendorLabel(v.trim()); }).join(', ');
|
|
1534
|
+
html += '<span class="session-tag" title="' + escapeHtml(fullList) + '">+' + (list.length - max) + '</span>';
|
|
1535
|
+
}
|
|
1536
|
+
return html;
|
|
1417
1537
|
}
|
|
1538
|
+
// ── Sessions Pagination State ──
|
|
1539
|
+
var allSessions = [];
|
|
1540
|
+
var sessionPage = 0;
|
|
1541
|
+
var sessionPageSize = 30;
|
|
1542
|
+
var sessionBindMap = {};
|
|
1543
|
+
var sessionAvailableVendors = [];
|
|
1544
|
+
|
|
1418
1545
|
async function updateSessions() {
|
|
1419
1546
|
try {
|
|
1420
1547
|
var results = await Promise.allSettled([
|
|
1421
|
-
fetchJSON('/api/dashboard/sessions?hours=24&limit=
|
|
1548
|
+
fetchJSON('/api/dashboard/sessions?hours=24&limit=200'),
|
|
1422
1549
|
fetchJSON('/api/session-vendor'),
|
|
1423
1550
|
fetchJSON('/api/status'),
|
|
1424
1551
|
]);
|
|
@@ -1426,38 +1553,87 @@ async function updateSessions() {
|
|
|
1426
1553
|
var data = results[0].value;
|
|
1427
1554
|
var bindData = results[1].status === 'fulfilled' ? results[1].value : {bindings: []};
|
|
1428
1555
|
var statusData = results[2].status === 'fulfilled' ? results[2].value : {tiers: []};
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1556
|
+
allSessions = data.sessions || [];
|
|
1557
|
+
sessionBindMap = {};
|
|
1558
|
+
(bindData.bindings || []).forEach(function(b) { sessionBindMap[b.session_key] = b.vendors; });
|
|
1559
|
+
sessionAvailableVendors = (statusData.tiers || []).map(function(t) { return t.name; });
|
|
1560
|
+
sessionPage = 0;
|
|
1561
|
+
renderSessionPage();
|
|
1562
|
+
} catch (e) {
|
|
1563
|
+
console.error('Sessions refresh error:', e);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function renderSessionPage() {
|
|
1568
|
+
var total = allSessions.length;
|
|
1569
|
+
var totalPages = Math.max(1, Math.ceil(total / sessionPageSize));
|
|
1570
|
+
if (sessionPage >= totalPages) sessionPage = totalPages - 1;
|
|
1571
|
+
var start = sessionPage * sessionPageSize;
|
|
1572
|
+
var page = allSessions.slice(start, start + sessionPageSize);
|
|
1573
|
+
var tbody = document.getElementById('sessions-tbody');
|
|
1574
|
+
|
|
1575
|
+
if (!total) {
|
|
1576
|
+
tbody.innerHTML = '<tr><td colspan="10" class="empty"><div class="empty-icon">📭</div>No session data</td></tr>';
|
|
1577
|
+
} else {
|
|
1578
|
+
tbody.innerHTML = page.map(function(s) {
|
|
1579
|
+
var parsed = parseSessionKey(s.session_key);
|
|
1580
|
+
var boundVendors = sessionBindMap[s.session_key];
|
|
1581
|
+
var selectHtml = buildBindSelect(s.session_key, boundVendors, sessionAvailableVendors);
|
|
1582
|
+
var modelsFull = (s.models || '').split(',').map(function(c){return c.trim();});
|
|
1583
|
+
var vendorsFull = (s.vendors || '').split(',').map(function(v){return formatVendorLabel(v.trim());});
|
|
1584
|
+
var sr = s.success_rate != null ? Math.round(s.success_rate) : null;
|
|
1585
|
+
return '<tr data-row onclick="toggleRow(this)">' +
|
|
1586
|
+
'<td class="session-key" onclick="event.stopPropagation()">' +
|
|
1587
|
+
'<div class="session-id" data-key="' + escapeHtml(s.session_key) + '" title="' + escapeHtml(s.session_key) + '">' +
|
|
1588
|
+
'<span class="session-id-text">' + escapeHtml(parsed.session_id || s.session_key) + '</span>' +
|
|
1589
|
+
'<button class="copy-btn" onclick="copyFromParent(this)" title="Copy Session ID">⧉</button>' +
|
|
1590
|
+
'</div>' +
|
|
1591
|
+
'<div class="session-meta" title="device: ' + escapeHtml(parsed.device_id) + ' | account: ' + escapeHtml(parsed.account_uuid) + '">' +
|
|
1592
|
+
'dev:' + escapeHtml(shortId(parsed.device_id, 8)) + ' · acct:' + escapeHtml(shortId(parsed.account_uuid, 8)) +
|
|
1593
|
+
'</div>' +
|
|
1594
|
+
'</td>' +
|
|
1447
1595
|
'<td>' + relativeTime(s.last_active_ts) + '</td>' +
|
|
1448
1596
|
'<td style="font-family:JetBrains Mono,monospace">' + fmtNum(s.total_requests) + '</td>' +
|
|
1449
1597
|
'<td style="font-family:JetBrains Mono,monospace">' + fmtTokens(s.total_tokens) + '</td>' +
|
|
1450
|
-
'<td>' + formatSessionTags(s.models,
|
|
1451
|
-
'<td>' + formatVendorTags(s.vendors) + '</td>' +
|
|
1452
|
-
'<td style="font-family:JetBrains Mono,monospace">' + (s.avg_duration_ms
|
|
1453
|
-
'<td>' + successBarHtml(s.success_rate) + '</td>' +
|
|
1454
|
-
'<td>' + selectHtml + '</td>' +
|
|
1598
|
+
'<td title="' + escapeHtml(modelsFull.join(', ')) + '">' + formatSessionTags(s.models, 3) + '</td>' +
|
|
1599
|
+
'<td title="' + escapeHtml(vendorsFull.join(', ')) + '">' + formatVendorTags(s.vendors) + '</td>' +
|
|
1600
|
+
'<td style="font-family:JetBrains Mono,monospace">' + fmtDuration(s.avg_duration_ms) + '</td>' +
|
|
1601
|
+
'<td class="cell-success">' + successBarHtml(s.success_rate) + '</td>' +
|
|
1602
|
+
'<td onclick="event.stopPropagation()">' + selectHtml + '</td>' +
|
|
1455
1603
|
'<td>' + formatCategories(s.client_categories) + '</td>' +
|
|
1456
|
-
'</tr>'
|
|
1604
|
+
'</tr>' +
|
|
1605
|
+
'<tr class="row-detail"><td colspan="10"><div class="detail-card">' +
|
|
1606
|
+
'<div class="detail-identity-row">' +
|
|
1607
|
+
'<div class="detail-item"><div class="detail-label">Session ID</div><div class="detail-value" title="' + escapeHtml(s.session_key) + '">' + escapeHtml(parsed.session_id || s.session_key) + '</div></div>' +
|
|
1608
|
+
'<div class="detail-item"><div class="detail-label">Device</div><div class="detail-value" title="' + escapeHtml(parsed.device_id || '') + '">' + (parsed.device_id ? escapeHtml(parsed.device_id) : '–') + '</div></div>' +
|
|
1609
|
+
'<div class="detail-item"><div class="detail-label">Account</div><div class="detail-value" title="' + escapeHtml(parsed.account_uuid || '') + '">' + (parsed.account_uuid ? escapeHtml(parsed.account_uuid) : '–') + '</div></div>' +
|
|
1610
|
+
'</div>' +
|
|
1611
|
+
'<div class="detail-metrics-grid">' +
|
|
1612
|
+
'<div class="detail-item"><div class="detail-label">Last Active</div><div class="detail-value">' + relativeTime(s.last_active_ts) + '</div></div>' +
|
|
1613
|
+
'<div class="detail-item"><div class="detail-label">Requests</div><div class="detail-value">' + fmtNum(s.total_requests) + '</div></div>' +
|
|
1614
|
+
'<div class="detail-item"><div class="detail-label">Tokens</div><div class="detail-value">' + fmtTokens(s.total_tokens) + '</div></div>' +
|
|
1615
|
+
'<div class="detail-item"><div class="detail-label">Models</div><div class="detail-value">' + (modelsFull.length ? modelsFull.map(function(m){return '<span class="session-tag">' + escapeHtml(m) + '</span>';}).join(' ') : '–') + '</div></div>' +
|
|
1616
|
+
'<div class="detail-item"><div class="detail-label">Vendors</div><div class="detail-value">' + (vendorsFull.length ? vendorsFull.map(function(v){return '<span class="session-tag">' + escapeHtml(v) + '</span>';}).join(' ') : '–') + '</div></div>' +
|
|
1617
|
+
'<div class="detail-item"><div class="detail-label">Avg Latency</div><div class="detail-value">' + fmtDuration(s.avg_duration_ms) + '</div></div>' +
|
|
1618
|
+
'<div class="detail-item" style="grid-column:span 2"><div class="detail-inline-pair">' +
|
|
1619
|
+
'<div><div class="detail-label">Success Rate</div><div class="detail-value">' + (sr != null ? sr + '%' : '–') + '</div></div>' +
|
|
1620
|
+
'<div><div class="detail-label">Client</div><div class="detail-value">' + escapeHtml(s.client_categories || '–') + '</div></div>' +
|
|
1621
|
+
'</div></div>' +
|
|
1622
|
+
'</div>' +
|
|
1623
|
+
'</div></td></tr>';
|
|
1457
1624
|
}).join('');
|
|
1458
|
-
} catch (e) {
|
|
1459
|
-
console.error('Sessions refresh error:', e);
|
|
1460
1625
|
}
|
|
1626
|
+
|
|
1627
|
+
document.getElementById('page-info').textContent = total + ' sessions';
|
|
1628
|
+
document.getElementById('page-num').textContent = (sessionPage + 1) + ' / ' + totalPages;
|
|
1629
|
+
document.getElementById('btn-prev').disabled = (sessionPage === 0);
|
|
1630
|
+
document.getElementById('btn-next').disabled = (sessionPage >= totalPages - 1);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function changePage(delta) {
|
|
1634
|
+
var totalPages = Math.max(1, Math.ceil(allSessions.length / sessionPageSize));
|
|
1635
|
+
sessionPage = Math.max(0, Math.min(totalPages - 1, sessionPage + delta));
|
|
1636
|
+
renderSessionPage();
|
|
1461
1637
|
}
|
|
1462
1638
|
|
|
1463
1639
|
function buildBindSelect(sessionKey, boundVendors, availableVendors) {
|
|
@@ -1515,7 +1691,7 @@ sessionsTbody.addEventListener('change', function(e) {
|
|
|
1515
1691
|
let refreshing = false;
|
|
1516
1692
|
let currentTab = 'overview';
|
|
1517
1693
|
const tabLoaded = { overview: false, sessions: false };
|
|
1518
|
-
const TAB_LABELS = { overview: 'Overview', sessions: '
|
|
1694
|
+
const TAB_LABELS = { overview: 'Overview', sessions: 'Sessions' };
|
|
1519
1695
|
|
|
1520
1696
|
async function refreshOverview() {
|
|
1521
1697
|
const days = currentDays > 0 ? currentDays : 7;
|
|
@@ -1606,9 +1782,7 @@ function switchTab(name) {
|
|
|
1606
1782
|
currentTab = name;
|
|
1607
1783
|
applyTabState(name);
|
|
1608
1784
|
syncTabUrl(name);
|
|
1609
|
-
|
|
1610
|
-
refresh();
|
|
1611
|
-
}
|
|
1785
|
+
refresh();
|
|
1612
1786
|
}
|
|
1613
1787
|
|
|
1614
1788
|
// ── 初始化 ────────────────────────────────────────────────
|
|
@@ -1621,6 +1795,10 @@ function switchTab(name) {
|
|
|
1621
1795
|
currentTab = initial;
|
|
1622
1796
|
applyTabState(initial);
|
|
1623
1797
|
syncTabUrl(initial);
|
|
1798
|
+
// Load version immediately regardless of active tab
|
|
1799
|
+
fetchJSON('/api/dashboard/summary?days=7').then(function(s) {
|
|
1800
|
+
if (s && s.version) document.getElementById('version-badge').textContent = 'v' + s.version;
|
|
1801
|
+
}).catch(function(){});
|
|
1624
1802
|
refresh(); // 仅加载初始页签的数据
|
|
1625
1803
|
setInterval(refresh, 600000); // 每 10 分钟刷新当前页签
|
|
1626
1804
|
})();
|
|
@@ -1807,7 +1985,7 @@ def register_dashboard_routes(app: Any) -> None:
|
|
|
1807
1985
|
media_type="application/json",
|
|
1808
1986
|
)
|
|
1809
1987
|
hours = max(1.0, min(hours, 168.0))
|
|
1810
|
-
limit = max(1, min(limit,
|
|
1988
|
+
limit = max(1, min(limit, 200))
|
|
1811
1989
|
try:
|
|
1812
1990
|
sessions = await token_logger.query_recent_sessions(
|
|
1813
1991
|
limit=limit, hours=hours
|
|
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
|