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.
Files changed (183) hide show
  1. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.gitignore +3 -0
  2. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/CHANGELOG.md +19 -8
  3. {coding_proxy-0.3.1a9 → 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.1a9 → coding_proxy-0.4.0}/pyproject.toml +1 -1
  6. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/dashboard.py +252 -74
  7. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/uv.lock +1 -1
  8. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.github/workflows/ci.yml +0 -0
  9. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.github/workflows/coverage.yml +0 -0
  10. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.github/workflows/release.yml +0 -0
  11. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/.pre-commit-config.yaml +0 -0
  12. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/AGENTS.md +0 -0
  13. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/CLAUDE.md +0 -0
  14. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/LICENSE +0 -0
  15. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/README.md +0 -0
  16. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/assets/dashboard-v0.2.4.png +0 -0
  17. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/config-reference.md +0 -0
  18. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/convert.md +0 -0
  19. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/design-patterns.md +0 -0
  20. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/routing.md +0 -0
  21. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/testing.md +0 -0
  22. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/arch/vendors.md +0 -0
  23. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/ci-cd.md +0 -0
  24. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/framework.md +0 -0
  25. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/api-reference.md +0 -0
  26. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/cli-reference.md +0 -0
  27. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/dashboard.md +0 -0
  28. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/monitoring.md +0 -0
  29. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/quickstart.md +0 -0
  30. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/guide/vendors.md +0 -0
  31. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/issue.md +0 -0
  32. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/user-guide.md +0 -0
  33. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/docs/zh-CN/README.md +0 -0
  34. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/__init__.py +0 -0
  35. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/__init__.py +0 -0
  36. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/__main__.py +0 -0
  37. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/__init__.py +0 -0
  38. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/__init__.py +0 -0
  39. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/base.py +0 -0
  40. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/github.py +0 -0
  41. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/providers/google.py +0 -0
  42. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/runtime.py +0 -0
  43. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/auth/store.py +0 -0
  44. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/cli/__init__.py +0 -0
  45. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/cli/auth_commands.py +0 -0
  46. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/cli/banner.py +0 -0
  47. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/compat/__init__.py +0 -0
  48. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/compat/canonical.py +0 -0
  49. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/compat/session_store.py +0 -0
  50. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/__init__.py +0 -0
  51. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/auth_schema.py +0 -0
  52. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/config.default.yaml +0 -0
  53. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/loader.py +0 -0
  54. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/resiliency.py +0 -0
  55. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/routing.py +0 -0
  56. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/schema.py +0 -0
  57. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/server.py +0 -0
  58. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/session_policy.py +0 -0
  59. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/config/vendors.py +0 -0
  60. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/__init__.py +0 -0
  61. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  62. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  63. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  64. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  65. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  66. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/convert/vendor_channels.py +0 -0
  67. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/__init__.py +0 -0
  68. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/db.py +0 -0
  69. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/formatters.py +0 -0
  70. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/logging/stats.py +0 -0
  71. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/__init__.py +0 -0
  72. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/auth.py +0 -0
  73. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/compat.py +0 -0
  74. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/constants.py +0 -0
  75. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/pricing.py +0 -0
  76. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/token.py +0 -0
  77. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/model/vendor.py +0 -0
  78. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/__init__.py +0 -0
  79. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/config.py +0 -0
  80. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  81. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  82. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  83. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  84. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/handler.py +0 -0
  85. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/operation.py +0 -0
  86. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/routes.py +0 -0
  87. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/native_api/usage_registry.py +0 -0
  88. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/pricing.py +0 -0
  89. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/__init__.py +0 -0
  90. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  91. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/error_classifier.py +0 -0
  92. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/executor.py +0 -0
  93. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/model_mapper.py +0 -0
  94. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/quota_guard.py +0 -0
  95. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/rate_limit.py +0 -0
  96. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/retry.py +0 -0
  97. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/router.py +0 -0
  98. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/session_manager.py +0 -0
  99. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/session_policy.py +0 -0
  100. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/tier.py +0 -0
  101. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/usage_parser.py +0 -0
  102. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/routing/usage_recorder.py +0 -0
  103. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/__init__.py +0 -0
  104. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/app.py +0 -0
  105. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/factory.py +0 -0
  106. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/responses.py +0 -0
  107. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/server/routes.py +0 -0
  108. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/streaming/__init__.py +0 -0
  109. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  110. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/__init__.py +0 -0
  111. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/alibaba.py +0 -0
  112. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/anthropic.py +0 -0
  113. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/antigravity.py +0 -0
  114. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/base.py +0 -0
  115. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot.py +0 -0
  116. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_models.py +0 -0
  117. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  118. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  119. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/doubao.py +0 -0
  120. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/kimi.py +0 -0
  121. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/minimax.py +0 -0
  122. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/mixins.py +0 -0
  123. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  124. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/token_manager.py +0 -0
  125. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/xiaomi.py +0 -0
  126. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/src/coding/proxy/vendors/zhipu.py +0 -0
  127. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/__init__.py +0 -0
  128. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_antigravity.py +0 -0
  129. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_app_routes.py +0 -0
  130. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_auto_login.py +0 -0
  131. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_banner.py +0 -0
  132. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_circuit_breaker.py +0 -0
  133. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_cli_usage.py +0 -0
  134. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_compat.py +0 -0
  135. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_config_init.py +0 -0
  136. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_config_loader.py +0 -0
  137. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_convert_request.py +0 -0
  138. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_convert_response.py +0 -0
  139. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_convert_sse.py +0 -0
  140. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot.py +0 -0
  141. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_convert_request.py +0 -0
  142. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_convert_response.py +0 -0
  143. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_models.py +0 -0
  144. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_copilot_urls.py +0 -0
  145. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_currency.py +0 -0
  146. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_error_classifier.py +0 -0
  147. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_logging_dual_write.py +0 -0
  148. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_mixins.py +0 -0
  149. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_auth.py +0 -0
  150. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_compat.py +0 -0
  151. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_constants.py +0 -0
  152. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_mapper.py +0 -0
  153. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_pricing.py +0 -0
  154. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_token.py +0 -0
  155. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_model_vendor.py +0 -0
  156. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_base_url_override.py +0 -0
  157. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_extractors.py +0 -0
  158. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_handler.py +0 -0
  159. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_operation.py +0 -0
  160. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_api_routes.py +0 -0
  161. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_native_vendors.py +0 -0
  162. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_parse_usage.py +0 -0
  163. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_parse_usage_gemini.py +0 -0
  164. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_pricing.py +0 -0
  165. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_quota_guard.py +0 -0
  166. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_rate_limit.py +0 -0
  167. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_router_chain.py +0 -0
  168. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_router_executor.py +0 -0
  169. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_runtime_reauth.py +0 -0
  170. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_schema.py +0 -0
  171. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_session_aware.py +0 -0
  172. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_streaming_anthropic_compat.py +0 -0
  173. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_tier.py +0 -0
  174. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_tiers_config.py +0 -0
  175. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_time_range.py +0 -0
  176. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_token_logger.py +0 -0
  177. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_token_logger_native_columns.py +0 -0
  178. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_token_manager.py +0 -0
  179. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_types.py +0 -0
  180. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_vendor_channels.py +0 -0
  181. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_vendor_streaming.py +0 -0
  182. {coding_proxy-0.3.1a9 → coding_proxy-0.4.0}/tests/test_vendors.py +0 -0
  183. {coding_proxy-0.3.1a9 → 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.1a9
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.1a9"
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);
@@ -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-x: auto; max-height: 480px; overflow-y: auto; }
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
- .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
- .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; }
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: 4px;
477
- margin-bottom: 16px;
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: none;
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: 14px;
544
+ font-size: 13px;
490
545
  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;
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 { 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
+ }
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
- <!-- Recent Active Sessions 页签 -->
672
+ <!-- Sessions 页签 -->
617
673
  <section class="tab-pane" id="tab-pane-sessions" role="tabpanel" aria-labelledby="tab-btn-sessions" data-tab="sessions">
618
- <!-- Recent Active Sessions -->
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 (_API_VENDORS.has(v)) {
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 `<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>` +
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) 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
+ }
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
- return vendors.split(',').map(function(v) {
1415
- 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>';
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=20'),
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
- var sessions = data.sessions || [];
1430
- var bindings = bindData.bindings || [];
1431
- var availableVendors = (statusData.tiers || []).map(function(t) { return t.name; });
1432
- var tbody = document.getElementById('sessions-tbody');
1433
- var subtitle = document.getElementById('sessions-subtitle');
1434
- if (subtitle) subtitle.textContent = 'Last ' + data.hours + 'h';
1435
- if (!sessions.length) {
1436
- tbody.innerHTML = '<tr><td colspan="10" class="empty"><div class="empty-icon">📭</div>No session data</td></tr>';
1437
- return;
1438
- }
1439
- // Build binding lookup: session_key → vendors list
1440
- var bindMap = {};
1441
- bindings.forEach(function(b) { bindMap[b.session_key] = b.vendors; });
1442
- tbody.innerHTML = sessions.map(function(s) {
1443
- var boundVendors = bindMap[s.session_key];
1444
- var selectHtml = buildBindSelect(s.session_key, boundVendors, availableVendors);
1445
- return '<tr>' +
1446
- '<td class="session-key" title="' + escapeHtml(s.session_key) + '">' + truncateKey(s.session_key, 22) + '</td>' +
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, 2) + '</td>' +
1451
- '<td>' + formatVendorTags(s.vendors) + '</td>' +
1452
- '<td style="font-family:JetBrains Mono,monospace">' + (s.avg_duration_ms ? Math.round(s.avg_duration_ms) + 'ms' : '–') + '</td>' +
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: 'Recent Active 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
- if (!tabLoaded[name]) {
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, 100))
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
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "coding-proxy"
77
- version = "0.3.1a9"
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