coding-proxy 0.3.1a10__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.
Files changed (183) hide show
  1. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/.gitignore +3 -0
  2. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/CHANGELOG.md +19 -8
  3. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/PKG-INFO +1 -1
  4. coding_proxy-0.4.0/assets/session-v0.4.0.png +0 -0
  5. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/pyproject.toml +1 -1
  6. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/server/dashboard.py +164 -61
  7. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/uv.lock +1 -1
  8. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/.github/workflows/ci.yml +0 -0
  9. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/.github/workflows/coverage.yml +0 -0
  10. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/.github/workflows/release.yml +0 -0
  11. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/.pre-commit-config.yaml +0 -0
  12. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/AGENTS.md +0 -0
  13. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/CLAUDE.md +0 -0
  14. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/LICENSE +0 -0
  15. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/README.md +0 -0
  16. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/assets/dashboard-v0.2.4.png +0 -0
  17. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/arch/config-reference.md +0 -0
  18. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/arch/convert.md +0 -0
  19. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/arch/design-patterns.md +0 -0
  20. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/arch/routing.md +0 -0
  21. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/arch/testing.md +0 -0
  22. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/arch/vendors.md +0 -0
  23. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/ci-cd.md +0 -0
  24. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/framework.md +0 -0
  25. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/guide/api-reference.md +0 -0
  26. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/guide/cli-reference.md +0 -0
  27. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/guide/dashboard.md +0 -0
  28. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/guide/monitoring.md +0 -0
  29. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/guide/quickstart.md +0 -0
  30. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/guide/vendors.md +0 -0
  31. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/issue.md +0 -0
  32. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/user-guide.md +0 -0
  33. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/docs/zh-CN/README.md +0 -0
  34. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/__init__.py +0 -0
  35. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/__init__.py +0 -0
  36. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/__main__.py +0 -0
  37. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/__init__.py +0 -0
  38. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/__init__.py +0 -0
  39. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/base.py +0 -0
  40. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/github.py +0 -0
  41. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/google.py +0 -0
  42. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/runtime.py +0 -0
  43. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/auth/store.py +0 -0
  44. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/cli/__init__.py +0 -0
  45. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/cli/auth_commands.py +0 -0
  46. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/cli/banner.py +0 -0
  47. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/compat/__init__.py +0 -0
  48. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/compat/canonical.py +0 -0
  49. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/compat/session_store.py +0 -0
  50. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/__init__.py +0 -0
  51. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/auth_schema.py +0 -0
  52. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/config.default.yaml +0 -0
  53. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/loader.py +0 -0
  54. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/resiliency.py +0 -0
  55. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/routing.py +0 -0
  56. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/schema.py +0 -0
  57. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/server.py +0 -0
  58. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/session_policy.py +0 -0
  59. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/config/vendors.py +0 -0
  60. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/__init__.py +0 -0
  61. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  62. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  63. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  64. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  65. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  66. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/convert/vendor_channels.py +0 -0
  67. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/logging/__init__.py +0 -0
  68. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/logging/db.py +0 -0
  69. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/logging/formatters.py +0 -0
  70. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/logging/stats.py +0 -0
  71. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/__init__.py +0 -0
  72. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/auth.py +0 -0
  73. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/compat.py +0 -0
  74. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/constants.py +0 -0
  75. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/pricing.py +0 -0
  76. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/token.py +0 -0
  77. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/model/vendor.py +0 -0
  78. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/__init__.py +0 -0
  79. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/config.py +0 -0
  80. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  81. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  82. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  83. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  84. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/handler.py +0 -0
  85. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/operation.py +0 -0
  86. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/routes.py +0 -0
  87. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/native_api/usage_registry.py +0 -0
  88. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/pricing.py +0 -0
  89. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/__init__.py +0 -0
  90. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  91. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/error_classifier.py +0 -0
  92. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/executor.py +0 -0
  93. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/model_mapper.py +0 -0
  94. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/quota_guard.py +0 -0
  95. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/rate_limit.py +0 -0
  96. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/retry.py +0 -0
  97. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/router.py +0 -0
  98. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/session_manager.py +0 -0
  99. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/session_policy.py +0 -0
  100. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/tier.py +0 -0
  101. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/usage_parser.py +0 -0
  102. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/routing/usage_recorder.py +0 -0
  103. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/server/__init__.py +0 -0
  104. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/server/app.py +0 -0
  105. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/server/factory.py +0 -0
  106. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/server/responses.py +0 -0
  107. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/server/routes.py +0 -0
  108. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/streaming/__init__.py +0 -0
  109. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  110. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/__init__.py +0 -0
  111. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/alibaba.py +0 -0
  112. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/anthropic.py +0 -0
  113. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/antigravity.py +0 -0
  114. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/base.py +0 -0
  115. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot.py +0 -0
  116. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_models.py +0 -0
  117. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  118. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  119. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/doubao.py +0 -0
  120. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/kimi.py +0 -0
  121. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/minimax.py +0 -0
  122. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/mixins.py +0 -0
  123. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  124. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/token_manager.py +0 -0
  125. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/xiaomi.py +0 -0
  126. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/src/coding/proxy/vendors/zhipu.py +0 -0
  127. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/__init__.py +0 -0
  128. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_antigravity.py +0 -0
  129. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_app_routes.py +0 -0
  130. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_auto_login.py +0 -0
  131. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_banner.py +0 -0
  132. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_circuit_breaker.py +0 -0
  133. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_cli_usage.py +0 -0
  134. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_compat.py +0 -0
  135. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_config_init.py +0 -0
  136. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_config_loader.py +0 -0
  137. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_convert_request.py +0 -0
  138. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_convert_response.py +0 -0
  139. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_convert_sse.py +0 -0
  140. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_copilot.py +0 -0
  141. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_copilot_convert_request.py +0 -0
  142. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_copilot_convert_response.py +0 -0
  143. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_copilot_models.py +0 -0
  144. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_copilot_urls.py +0 -0
  145. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_currency.py +0 -0
  146. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_error_classifier.py +0 -0
  147. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_logging_dual_write.py +0 -0
  148. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_mixins.py +0 -0
  149. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_auth.py +0 -0
  150. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_compat.py +0 -0
  151. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_constants.py +0 -0
  152. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_mapper.py +0 -0
  153. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_pricing.py +0 -0
  154. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_token.py +0 -0
  155. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_model_vendor.py +0 -0
  156. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_native_api_base_url_override.py +0 -0
  157. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_native_api_extractors.py +0 -0
  158. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_native_api_handler.py +0 -0
  159. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_native_api_operation.py +0 -0
  160. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_native_api_routes.py +0 -0
  161. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_native_vendors.py +0 -0
  162. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_parse_usage.py +0 -0
  163. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_parse_usage_gemini.py +0 -0
  164. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_pricing.py +0 -0
  165. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_quota_guard.py +0 -0
  166. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_rate_limit.py +0 -0
  167. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_router_chain.py +0 -0
  168. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_router_executor.py +0 -0
  169. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_runtime_reauth.py +0 -0
  170. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_schema.py +0 -0
  171. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_session_aware.py +0 -0
  172. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_streaming_anthropic_compat.py +0 -0
  173. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_tier.py +0 -0
  174. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_tiers_config.py +0 -0
  175. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_time_range.py +0 -0
  176. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_token_logger.py +0 -0
  177. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_token_logger_native_columns.py +0 -0
  178. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_token_manager.py +0 -0
  179. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_types.py +0 -0
  180. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_vendor_channels.py +0 -0
  181. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_vendor_streaming.py +0 -0
  182. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_vendors.py +0 -0
  183. {coding_proxy-0.3.1a10 → coding_proxy-0.4.0}/tests/test_zhipu.py +0 -0
@@ -23,5 +23,8 @@ config.yaml
23
23
  .claude/.prompts.md
24
24
  .python-version
25
25
 
26
+ # Playwright MCP
27
+ .playwright-mcp/
28
+
26
29
  # Log files (dual-write logging)
27
30
  coding-proxy.log*
@@ -4,16 +4,27 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- - fix(vendor-channels): 为所有 targeting zhipu 的转换通道(zhipu→zhipu、copilot→zhipu、anthropic→zhipu)新增 `tool_result.id` 字段注入,修复 zhipu GLM-5 后端错误访问 `.id` 属性(`'ClaudeContentBlockToolResult' object has no attribute 'id'`)导致的 500 错误,使 zhipu 可完全承接含 tool_result 的会话;
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
- ### Bug Fixes
9
+ > [!IMPORTANT]
10
+ >
11
+ > **🚀 Session 级专属路由策略!**
12
+ >
13
+ > 给每个 Session 指定专属的 vendor,动态调节不同 vendors 间的 LLM 流量。
14
+
15
+ ![session](assets/session-v0.4.0.png)
16
+
17
+ ### ✨ 核心亮点
18
+
19
+ - feat(session-policy): 新增 Session 级专属路由策略 (#219)
20
+ - feat(dashboard): 新增会话活动面板 (#222)
21
+
22
+ ### 🔧 更多特性
13
23
 
14
- - fix(vendor-channels): 新增 `anthropic zhipu` 跨供应商转换通道,修复 Anthropic beta 功能(web search, computer use)产生的 `server_tool_use` 块导致 zhipu 400 错误的问题;
15
- - fix(error-classifier): 增强语义拒绝检测,识别 zhipu 等供应商返回的中文错误消息(如「API 调用参数有误」code=1210),确保正确触发故障转移;
16
- - fix(vendor-channels): `_remove_vendor_blocks` 增加空内容占位保护,防止内容块全部剥离后消息结构不合法。
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.1a10
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.3.1a10"
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: 16px;
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; margin: 0 10px; max-width: 100px; }
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);
@@ -407,9 +408,14 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
407
408
  border-bottom: 1px solid var(--border);
408
409
  }
409
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-table .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; white-space: normal; overflow: visible; }
412
- .session-id { line-height: 1.4; word-break: break-all; }
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; }
413
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; }
414
420
  .session-tag {
415
421
  display: inline-block; font-size: 11px; padding: 2px 7px;
@@ -417,8 +423,43 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
417
423
  background: rgba(88,166,255,.08); border: 1px solid rgba(88,166,255,.15);
418
424
  color: var(--text-secondary);
419
425
  }
420
- .success-bar { width: 56px; height: 4px; border-radius: 2px; background: rgba(255,255,255,.06); display: inline-block; vertical-align: middle; margin-left: 6px; }
421
- .success-bar-fill { height: 100%; border-radius: 2px; }
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; }
422
463
  /* ── Vendor Bind 选择器 ── */
423
464
  .bind-select {
424
465
  padding: 3px 6px; border-radius: 6px;
@@ -490,28 +531,28 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
490
531
  /* ── Tabs ─────────────────────────────────────────────────── */
491
532
  .tabs {
492
533
  display: flex;
493
- gap: 4px;
494
- margin-bottom: 16px;
495
- border-bottom: 1px solid var(--border);
496
- padding: 0 2px;
534
+ gap: 2px;
535
+ padding: 0;
497
536
  }
498
537
  .tab-btn {
499
538
  appearance: none;
500
539
  background: transparent;
501
- border: none;
502
- border-bottom: 2px solid transparent;
540
+ border: 1px solid transparent;
503
541
  color: var(--text-secondary);
504
542
  cursor: pointer;
505
543
  font-family: inherit;
506
- font-size: 14px;
544
+ font-size: 13px;
507
545
  font-weight: 500;
508
- padding: 10px 16px;
509
- margin-bottom: -1px;
510
- transition: color .15s ease, border-color .15s ease, background .15s ease;
511
- 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);
512
549
  }
513
550
  .tab-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); }
514
- .tab-btn.active { color: var(--text-primary); border-bottom-color: var(--accent-blue); }
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
+ }
515
556
  .tab-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
516
557
  .tab-pane { display: none; }
517
558
  .tab-pane.active { display: block; }
@@ -525,18 +566,16 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
525
566
  <span class="badge" id="version-badge">v-.-.-</span>
526
567
  </div>
527
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>
528
573
  <span class="refresh-time" id="refresh-time">正在加载…</span>
529
574
  <button class="btn-refresh" onclick="refresh()">⟳ 刷新</button>
530
575
  </div>
531
576
  </header>
532
577
 
533
578
  <main>
534
- <!-- 页签导航 -->
535
- <nav class="tabs" role="tablist" aria-label="Dashboard sections">
536
- <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>
537
- <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>
538
- </nav>
539
-
540
579
  <!-- Overview 页签 -->
541
580
  <section class="tab-pane active" id="tab-pane-overview" role="tabpanel" aria-labelledby="tab-btn-overview" data-tab="overview">
542
581
  <!-- 时间区间选择器 -->
@@ -630,27 +669,23 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
630
669
  </div>
631
670
  </section>
632
671
 
633
- <!-- Recent Active Sessions 页签 -->
672
+ <!-- Sessions 页签 -->
634
673
  <section class="tab-pane" id="tab-pane-sessions" role="tabpanel" aria-labelledby="tab-btn-sessions" data-tab="sessions">
635
- <!-- Recent Active Sessions -->
674
+ <!-- Sessions -->
636
675
  <div class="card sessions-card">
637
- <div class="card-title">
638
- <span>Recent Active Sessions</span>
639
- <span style="font-size:12px;color:var(--text-tertiary)" id="sessions-subtitle">Last 24h</span>
640
- </div>
641
676
  <div class="session-table-wrap" id="sessions-table-wrap">
642
677
  <table class="session-table">
643
678
  <colgroup>
644
- <col style="width:22%">
645
- <col style="width:8%">
679
+ <col style="width:12%">
646
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%">
647
685
  <col style="width:7%">
648
- <col style="width:14%">
686
+ <col style="width:9%">
687
+ <col style="width:12%">
649
688
  <col style="width:12%">
650
- <col style="width:8%">
651
- <col style="width:8%">
652
- <col style="width:8%">
653
- <col style="width:6%">
654
689
  </colgroup>
655
690
  <thead>
656
691
  <tr>
@@ -721,7 +756,32 @@ function fmtTokens(n) {
721
756
  return String(n);
722
757
  }
723
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
+ }
724
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
+ }
725
785
  function now() {
726
786
  return new Date().toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
727
787
  }
@@ -731,12 +791,15 @@ function now() {
731
791
  // _API_VENDORS 需与后端 native_api/handler.py::_VENDOR_LABEL 对齐,
732
792
  // 新增无 -native 后缀的 native vendor 时同步更新本集合。
733
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
+ }
734
800
  function formatVendorLabel(v) {
735
801
  if (!isValidLabel(v)) return v;
736
- if (_API_VENDORS.has(v)) {
737
- const name = v.endsWith('-native') ? v.slice(0, -'-native'.length) : v;
738
- return 'api | ' + name;
739
- }
802
+ if (isApiVendor(v)) return 'api | ' + vendorShortName(v);
740
803
  return 'cc | ' + v;
741
804
  }
742
805
 
@@ -1027,10 +1090,11 @@ function renderQuotaBar(qg) {
1027
1090
  if (!qg || qg.usage_percent == null) return '';
1028
1091
  const pct = Math.round(qg.usage_percent);
1029
1092
  const label = quotaWindowLabel(qg.window_hours);
1030
- return `<span class="status-badge ${quotaClass(pct)}">${label} ${pct}%</span>` +
1093
+ return `<div class="quota-group">` +
1094
+ `<span class="status-badge ${quotaClass(pct)}">${label} ${pct}%</span>` +
1031
1095
  `<div class="quota-bar-wrap"><div class="quota-bar-bg">` +
1032
1096
  `<div class="quota-bar-fill" style="width:${Math.min(pct,100)}%;background:${quotaBarColor(pct)}"></div>` +
1033
- `</div></div>`;
1097
+ `</div></div></div>`;
1034
1098
  }
1035
1099
 
1036
1100
  function updateVendorStatus(status) {
@@ -1440,7 +1504,10 @@ function formatSessionTags(str, max) {
1440
1504
  var html = list.slice(0, max).map(function(c) {
1441
1505
  return '<span class="session-tag">' + escapeHtml(c.trim()) + '</span>';
1442
1506
  }).join('');
1443
- if (list.length > max) html += '<span class="session-tag">+' + (list.length - max) + '</span>';
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
+ }
1444
1511
  return html;
1445
1512
  }
1446
1513
  function formatCategories(cats) {
@@ -1453,9 +1520,20 @@ function formatCategories(cats) {
1453
1520
  }
1454
1521
  function formatVendorTags(vendors) {
1455
1522
  if (!vendors) return '–';
1456
- return vendors.split(',').map(function(v) {
1457
- return '<span class="session-tag">' + formatVendorLabel(v.trim()) + '</span>';
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>';
1458
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;
1459
1537
  }
1460
1538
  // ── Sessions Pagination State ──
1461
1539
  var allSessions = [];
@@ -1479,8 +1557,6 @@ async function updateSessions() {
1479
1557
  sessionBindMap = {};
1480
1558
  (bindData.bindings || []).forEach(function(b) { sessionBindMap[b.session_key] = b.vendors; });
1481
1559
  sessionAvailableVendors = (statusData.tiers || []).map(function(t) { return t.name; });
1482
- var subtitle = document.getElementById('sessions-subtitle');
1483
- if (subtitle) subtitle.textContent = 'Last ' + data.hours + 'h';
1484
1560
  sessionPage = 0;
1485
1561
  renderSessionPage();
1486
1562
  } catch (e) {
@@ -1503,9 +1579,15 @@ function renderSessionPage() {
1503
1579
  var parsed = parseSessionKey(s.session_key);
1504
1580
  var boundVendors = sessionBindMap[s.session_key];
1505
1581
  var selectHtml = buildBindSelect(s.session_key, boundVendors, sessionAvailableVendors);
1506
- return '<tr>' +
1507
- '<td class="session-key">' +
1508
- '<div class="session-id" title="' + escapeHtml(s.session_key) + '">' + escapeHtml(parsed.session_id || s.session_key) + '</div>' +
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>' +
1509
1591
  '<div class="session-meta" title="device: ' + escapeHtml(parsed.device_id) + ' | account: ' + escapeHtml(parsed.account_uuid) + '">' +
1510
1592
  'dev:' + escapeHtml(shortId(parsed.device_id, 8)) + ' · acct:' + escapeHtml(shortId(parsed.account_uuid, 8)) +
1511
1593
  '</div>' +
@@ -1513,13 +1595,32 @@ function renderSessionPage() {
1513
1595
  '<td>' + relativeTime(s.last_active_ts) + '</td>' +
1514
1596
  '<td style="font-family:JetBrains Mono,monospace">' + fmtNum(s.total_requests) + '</td>' +
1515
1597
  '<td style="font-family:JetBrains Mono,monospace">' + fmtTokens(s.total_tokens) + '</td>' +
1516
- '<td>' + formatSessionTags(s.models, 2) + '</td>' +
1517
- '<td>' + formatVendorTags(s.vendors) + '</td>' +
1518
- '<td style="font-family:JetBrains Mono,monospace">' + (s.avg_duration_ms ? Math.round(s.avg_duration_ms) + 'ms' : '–') + '</td>' +
1519
- '<td>' + successBarHtml(s.success_rate) + '</td>' +
1520
- '<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>' +
1521
1603
  '<td>' + formatCategories(s.client_categories) + '</td>' +
1522
- '</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>';
1523
1624
  }).join('');
1524
1625
  }
1525
1626
 
@@ -1590,7 +1691,7 @@ sessionsTbody.addEventListener('change', function(e) {
1590
1691
  let refreshing = false;
1591
1692
  let currentTab = 'overview';
1592
1693
  const tabLoaded = { overview: false, sessions: false };
1593
- const TAB_LABELS = { overview: 'Overview', sessions: 'Recent Active Sessions' };
1694
+ const TAB_LABELS = { overview: 'Overview', sessions: 'Sessions' };
1594
1695
 
1595
1696
  async function refreshOverview() {
1596
1697
  const days = currentDays > 0 ? currentDays : 7;
@@ -1681,9 +1782,7 @@ function switchTab(name) {
1681
1782
  currentTab = name;
1682
1783
  applyTabState(name);
1683
1784
  syncTabUrl(name);
1684
- if (!tabLoaded[name]) {
1685
- refresh();
1686
- }
1785
+ refresh();
1687
1786
  }
1688
1787
 
1689
1788
  // ── 初始化 ────────────────────────────────────────────────
@@ -1696,6 +1795,10 @@ function switchTab(name) {
1696
1795
  currentTab = initial;
1697
1796
  applyTabState(initial);
1698
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(){});
1699
1802
  refresh(); // 仅加载初始页签的数据
1700
1803
  setInterval(refresh, 600000); // 每 10 分钟刷新当前页签
1701
1804
  })();
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "coding-proxy"
77
- version = "0.3.1a10"
77
+ version = "0.4.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes