coding-proxy 0.2.3a3__tar.gz → 0.2.3a5__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 (147) hide show
  1. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/PKG-INFO +1 -1
  2. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/pyproject.toml +1 -1
  3. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/config.default.yaml +1 -1
  4. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/logging/db.py +1 -1
  5. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/executor.py +6 -1
  6. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/quota_guard.py +1 -0
  7. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/dashboard.py +161 -95
  8. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/uv.lock +1 -1
  9. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/.github/workflows/ci.yml +0 -0
  10. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/.github/workflows/coverage.yml +0 -0
  11. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/.github/workflows/release.yml +0 -0
  12. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/.gitignore +0 -0
  13. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/AGENTS.md +0 -0
  14. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/CHANGELOG.md +0 -0
  15. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/CLAUDE.md +0 -0
  16. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/LICENSE +0 -0
  17. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/README.md +0 -0
  18. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/docs/ci-cd.md +0 -0
  19. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/docs/framework.md +0 -0
  20. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/docs/user-guide.md +0 -0
  21. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/docs/zh-CN/README.md +0 -0
  22. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/__init__.py +0 -0
  23. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/__init__.py +0 -0
  24. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/__main__.py +0 -0
  25. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/__init__.py +0 -0
  26. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/providers/__init__.py +0 -0
  27. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/providers/base.py +0 -0
  28. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/providers/github.py +0 -0
  29. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/providers/google.py +0 -0
  30. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/runtime.py +0 -0
  31. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/auth/store.py +0 -0
  32. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/cli/__init__.py +0 -0
  33. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/cli/auth_commands.py +0 -0
  34. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/cli/banner.py +0 -0
  35. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/compat/__init__.py +0 -0
  36. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/compat/canonical.py +0 -0
  37. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/compat/session_store.py +0 -0
  38. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/__init__.py +0 -0
  39. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/auth_schema.py +0 -0
  40. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/loader.py +0 -0
  41. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/resiliency.py +0 -0
  42. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/routing.py +0 -0
  43. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/schema.py +0 -0
  44. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/server.py +0 -0
  45. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/config/vendors.py +0 -0
  46. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/convert/__init__.py +0 -0
  47. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  48. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  49. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  50. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  51. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  52. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/logging/__init__.py +0 -0
  53. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/logging/formatters.py +0 -0
  54. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/logging/stats.py +0 -0
  55. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/__init__.py +0 -0
  56. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/auth.py +0 -0
  57. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/compat.py +0 -0
  58. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/constants.py +0 -0
  59. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/pricing.py +0 -0
  60. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/token.py +0 -0
  61. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/model/vendor.py +0 -0
  62. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/pricing.py +0 -0
  63. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/__init__.py +0 -0
  64. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  65. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/error_classifier.py +0 -0
  66. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/model_mapper.py +0 -0
  67. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/rate_limit.py +0 -0
  68. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/retry.py +0 -0
  69. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/router.py +0 -0
  70. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/session_manager.py +0 -0
  71. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/tier.py +0 -0
  72. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/usage_parser.py +0 -0
  73. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/routing/usage_recorder.py +0 -0
  74. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/__init__.py +0 -0
  75. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/app.py +0 -0
  76. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/factory.py +0 -0
  77. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/request_normalizer.py +0 -0
  78. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/responses.py +0 -0
  79. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/server/routes.py +0 -0
  80. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/streaming/__init__.py +0 -0
  81. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  82. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/__init__.py +0 -0
  83. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/alibaba.py +0 -0
  84. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/anthropic.py +0 -0
  85. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/antigravity.py +0 -0
  86. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/base.py +0 -0
  87. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/copilot.py +0 -0
  88. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/copilot_models.py +0 -0
  89. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  90. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  91. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/doubao.py +0 -0
  92. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/kimi.py +0 -0
  93. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/minimax.py +0 -0
  94. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/mixins.py +0 -0
  95. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  96. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/token_manager.py +0 -0
  97. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/xiaomi.py +0 -0
  98. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/src/coding/proxy/vendors/zhipu.py +0 -0
  99. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/__init__.py +0 -0
  100. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_antigravity.py +0 -0
  101. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_app_routes.py +0 -0
  102. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_auto_login.py +0 -0
  103. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_banner.py +0 -0
  104. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_circuit_breaker.py +0 -0
  105. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_cli_usage.py +0 -0
  106. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_compat.py +0 -0
  107. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_config_init.py +0 -0
  108. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_config_loader.py +0 -0
  109. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_convert_request.py +0 -0
  110. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_convert_response.py +0 -0
  111. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_convert_sse.py +0 -0
  112. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_copilot.py +0 -0
  113. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_copilot_convert_request.py +0 -0
  114. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_copilot_convert_response.py +0 -0
  115. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_copilot_models.py +0 -0
  116. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_copilot_urls.py +0 -0
  117. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_currency.py +0 -0
  118. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_error_classifier.py +0 -0
  119. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_logging_dual_write.py +0 -0
  120. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_mixins.py +0 -0
  121. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_auth.py +0 -0
  122. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_compat.py +0 -0
  123. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_constants.py +0 -0
  124. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_mapper.py +0 -0
  125. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_pricing.py +0 -0
  126. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_token.py +0 -0
  127. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_model_vendor.py +0 -0
  128. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_native_vendors.py +0 -0
  129. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_parse_usage.py +0 -0
  130. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_pricing.py +0 -0
  131. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_quota_guard.py +0 -0
  132. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_rate_limit.py +0 -0
  133. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_request_normalizer.py +0 -0
  134. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_router_chain.py +0 -0
  135. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_router_executor.py +0 -0
  136. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_runtime_reauth.py +0 -0
  137. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_schema.py +0 -0
  138. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_streaming_anthropic_compat.py +0 -0
  139. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_tier.py +0 -0
  140. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_tiers_config.py +0 -0
  141. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_time_range.py +0 -0
  142. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_token_logger.py +0 -0
  143. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_token_manager.py +0 -0
  144. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_types.py +0 -0
  145. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_vendor_streaming.py +0 -0
  146. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_vendors.py +0 -0
  147. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a5}/tests/test_zhipu.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.2.3a3
3
+ Version: 0.2.3a5
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.2.3a3"
3
+ version = "0.2.3a5"
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"
@@ -58,7 +58,7 @@ vendors:
58
58
  probe_interval_seconds: 300
59
59
  weekly_quota_guard:
60
60
  enabled: true
61
- token_budget: 250000000 # 一周 token 预算(根据订阅计划调整)
61
+ token_budget: 500000000 # 一周 token 预算(根据订阅计划调整)
62
62
  window_hours: 168.0 # 7 天滑动窗口
63
63
  threshold_percent: 99.0
64
64
  probe_interval_seconds: 1800 # 每 30 分钟探测一次
@@ -497,7 +497,7 @@ class TokenLogger:
497
497
  return 0
498
498
  cutoff_iso = _hours_ago_utc_iso(window_hours)
499
499
  cursor = await self._db.execute(
500
- """SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total
500
+ """SELECT COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total
501
501
  FROM usage_log
502
502
  WHERE vendor = ? AND success = 1
503
503
  AND ts >= ?""",
@@ -276,7 +276,12 @@ class _RouteExecutor:
276
276
  tier.name,
277
277
  usage,
278
278
  )
279
- tier.record_success(info.input_tokens + info.output_tokens)
279
+ tier.record_success(
280
+ info.input_tokens
281
+ + info.output_tokens
282
+ + info.cache_creation_tokens
283
+ + info.cache_read_tokens
284
+ )
280
285
  duration = int((time.monotonic() - start) * 1000)
281
286
  model = body.get("model", "unknown")
282
287
  model_served = usage.get("model_served") or tier.vendor.map_model(model)
@@ -197,6 +197,7 @@ class QuotaGuard:
197
197
  if self._budget > 0
198
198
  else 0,
199
199
  "threshold_percent": self._threshold * 100,
200
+ "window_hours": self.window_hours,
200
201
  }
201
202
 
202
203
  def _expire(self) -> None:
@@ -19,14 +19,20 @@ def _build_favicon() -> bytes:
19
19
 
20
20
  width, height = 16, 16
21
21
  pixel_rows: list[bytes] = []
22
+ cx, cy = width / 2.0, height / 2.0
22
23
  for y in range(height - 1, -1, -1): # BMP bottom-up
23
24
  row = bytearray()
24
25
  for x in range(width):
25
- t = (x + (height - 1 - y)) / (width + height - 2)
26
- r = int(88 + (188 - 88) * t)
27
- g = int(166 + (140 - 166) * t)
28
- b = 255
29
- row.extend([b, g, r, 255]) # BGRA
26
+ dx = x - cx + 0.5
27
+ dy = y - cy + 0.5
28
+ if dx * dx + dy * dy > (width / 2.0) ** 2:
29
+ row.extend([0, 0, 0, 0]) # 圆外透明
30
+ else:
31
+ t = (x + (height - 1 - y)) / (width + height - 2)
32
+ r = int(88 + (188 - 88) * t)
33
+ g = int(166 + (140 - 166) * t)
34
+ b = 255
35
+ row.extend([b, g, r, 255]) # BGRA
30
36
  pixel_rows.append(bytes(row))
31
37
 
32
38
  bmp_hdr = struct.pack(
@@ -114,7 +120,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
114
120
  .logo {
115
121
  width: 30px; height: 30px;
116
122
  background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
117
- border-radius: 8px;
123
+ border-radius: 50%;
118
124
  display: flex; align-items: center; justify-content: center;
119
125
  font-size: 15px; font-weight: 700; color: #fff;
120
126
  box-shadow: 0 2px 8px rgba(88,166,255,.3);
@@ -148,7 +154,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
148
154
  /* ── KPI 卡片 ── */
149
155
  .kpi-grid {
150
156
  display: grid;
151
- grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
157
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
152
158
  gap: 12px;
153
159
  margin-bottom: 18px;
154
160
  }
@@ -189,7 +195,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
189
195
  font-family: 'JetBrains Mono', monospace;
190
196
  letter-spacing: -0.5px;
191
197
  }
192
- .kpi-sub { font-size: 11px; color: var(--text-tertiary); margin-top: 5px; }
198
+ .kpi-sub { font-size: 11px; color: var(--text-tertiary); margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
193
199
  .color-blue { color: var(--accent-blue); }
194
200
  .color-green { color: var(--accent-green); }
195
201
  .color-yellow { color: var(--accent-yellow); }
@@ -446,33 +452,32 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
446
452
  </div>
447
453
  </div>
448
454
 
449
- <!-- 故障转移明细表 -->
450
- <div class="card">
451
- <div class="card-title">故障转移明细</div>
452
- <div class="ft-table-wrap">
453
- <table>
454
- <thead>
455
- <tr>
456
- <th>来源供应商</th>
457
- <th>目标供应商</th>
458
- <th>次数</th>
459
- </tr>
460
- </thead>
461
- <tbody id="ft-tbody">
462
- <tr><td colspan="3" class="empty">加载中…</td></tr>
463
- </tbody>
464
- </table>
465
- </div>
466
- </div>
467
455
  </main>
468
456
 
469
457
  <script>
470
458
  // ── 颜色配置 ──────────────────────────────────────────────
459
+ // 调色盘参考 Tailwind CSS 400-level,深色背景高区分度最佳实践
471
460
  const VENDOR_COLORS = [
472
- '#58a6ff','#bc8cff','#3fb950','#ffa657','#f85149',
473
- '#79c0ff','#d2a8ff','#56d364','#ffb77c','#ff7b72',
474
- '#39d353','#e3b341','#a5d6ff','#f0a9eb','#7ee787',
475
- '#ffa198','#cae8ff','#dbedff','#b6e3ff','#54aeff',
461
+ '#60A5FA', // blue-400
462
+ '#FB923C', // orange-400
463
+ '#34D399', // emerald-400
464
+ '#A78BFA', // violet-400
465
+ '#F87171', // red-400
466
+ '#38BDF8', // sky-400
467
+ '#FBBF24', // amber-400
468
+ '#F472B6', // pink-400
469
+ '#4ADE80', // green-400
470
+ '#E879F9', // fuchsia-400
471
+ '#818CF8', // indigo-400
472
+ '#2DD4BF', // teal-400
473
+ '#FB7185', // rose-400
474
+ '#FCD34D', // yellow-300
475
+ '#6EE7B7', // emerald-300
476
+ '#C4B5FD', // violet-300
477
+ '#7DD3FC', // sky-300
478
+ '#FED7AA', // orange-200
479
+ '#FECDD3', // rose-200
480
+ '#BBF7D0', // green-200
476
481
  ];
477
482
 
478
483
  // ── 工具函数 ──────────────────────────────────────────────
@@ -502,12 +507,72 @@ Chart.defaults.color = '#8b949e';
502
507
  Chart.defaults.borderColor = 'rgba(255,255,255,.04)';
503
508
  Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
504
509
  Chart.defaults.font.size = 11;
510
+ Chart.defaults.plugins.tooltip.usePointStyle = true;
505
511
 
506
512
  const COMMON_SCALE_X = { grid: { display: false }, ticks: { maxTicksLimit: 10 } };
507
513
  const COMMON_SCALE_Y = { grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true };
508
- const COMMON_LEGEND = { position: 'bottom', labels: { boxWidth: 8, padding: 14, usePointStyle: true, pointStyleWidth: 8, font: { size: 11 } } };
514
+ const COMMON_LEGEND = {
515
+ position: 'bottom',
516
+ labels: {
517
+ boxWidth: 8,
518
+ padding: 14,
519
+ usePointStyle: true,
520
+ pointStyle: 'circle',
521
+ pointStyleWidth: 8,
522
+ font: { size: 11 },
523
+ generateLabels: chart => {
524
+ const items = Chart.defaults.plugins.legend.labels.generateLabels(chart);
525
+ items.forEach(item => { item.pointStyle = 'circle'; item.lineWidth = 0; item.fillStyle = item.strokeStyle; });
526
+ return items;
527
+ },
528
+ },
529
+ };
509
530
  const COMMON_LINE_DATASET = { tension: .35, pointRadius: 0, pointHoverRadius: 5, borderWidth: 2 };
510
531
 
532
+ // ── Legend 点击交互:单击=仅选该项,Ctrl/Meta+单击=多选追加,Shift+单击=排除 ──
533
+ function legendOnClick(e, legendItem, legend) {
534
+ const chart = legend.chart;
535
+ const isShift = e.native.shiftKey;
536
+ const isCtrl = e.native.ctrlKey || e.native.metaKey;
537
+ if (chart.config.type === 'doughnut' || chart.config.type === 'pie') {
538
+ const idx = legendItem.index;
539
+ const dataLen = chart.data.labels.length;
540
+ if (isShift) {
541
+ chart.toggleDataVisibility(idx);
542
+ } else if (isCtrl) {
543
+ if (!chart.getDataVisibility(idx)) chart.toggleDataVisibility(idx);
544
+ } else {
545
+ const allOthersHidden = [...Array(dataLen).keys()].filter(i => i !== idx).every(i => !chart.getDataVisibility(i));
546
+ if (allOthersHidden) {
547
+ for (let i = 0; i < dataLen; i++) { if (!chart.getDataVisibility(i)) chart.toggleDataVisibility(i); }
548
+ } else {
549
+ for (let i = 0; i < dataLen; i++) {
550
+ const vis = chart.getDataVisibility(i);
551
+ if (i === idx && !vis) chart.toggleDataVisibility(i);
552
+ if (i !== idx && vis) chart.toggleDataVisibility(i);
553
+ }
554
+ }
555
+ }
556
+ } else {
557
+ const idx = legendItem.datasetIndex;
558
+ const datasets = chart.data.datasets;
559
+ if (isShift) {
560
+ const meta = chart.getDatasetMeta(idx);
561
+ meta.hidden = !meta.hidden;
562
+ } else if (isCtrl) {
563
+ chart.getDatasetMeta(idx).hidden = false;
564
+ } else {
565
+ const allOthersHidden = datasets.every((_, i) => i === idx || !!chart.getDatasetMeta(i).hidden);
566
+ if (allOthersHidden) {
567
+ datasets.forEach((_, i) => { chart.getDatasetMeta(i).hidden = false; });
568
+ } else {
569
+ datasets.forEach((_, i) => { chart.getDatasetMeta(i).hidden = (i !== idx); });
570
+ }
571
+ }
572
+ }
573
+ chart.update();
574
+ }
575
+
511
576
  // ── 图表实例 ──────────────────────────────────────────────
512
577
  let chartTimeline = null;
513
578
  let chartVendorDist = null;
@@ -529,28 +594,29 @@ async function fetchJSON(url) {
529
594
 
530
595
  // ── KPI 更新 ──────────────────────────────────────────────
531
596
  function updateKPI(summary) {
532
- const t = summary.today, w = summary.week;
597
+ const t = summary.today, r = summary.range;
598
+ const lbl = currentRangeLabel;
533
599
 
534
600
  document.getElementById('kpi-req-today').textContent = fmtNum(t.requests);
535
- document.getElementById('kpi-req-week').textContent = '本周 ' + fmtNum(w.requests);
601
+ document.getElementById('kpi-req-week').textContent = lbl + ' ' + fmtNum(r.requests);
536
602
 
537
- const tokT = t.tokens, tokW = w.tokens;
603
+ const tokT = t.tokens, tokR = r.tokens;
538
604
  const totalT = tokT.input + tokT.output + tokT.cache_creation + tokT.cache_read;
539
- const totalW = tokW.input + tokW.output + tokW.cache_creation + tokW.cache_read;
605
+ const totalR = tokR.input + tokR.output + tokR.cache_creation + tokR.cache_read;
540
606
  document.getElementById('kpi-tok-today').textContent = fmtTokens(totalT);
541
- document.getElementById('kpi-tok-week').textContent = '本周 ' + fmtTokens(totalW);
607
+ document.getElementById('kpi-tok-week').textContent = lbl + ' ' + fmtTokens(totalR);
542
608
 
543
609
  document.getElementById('kpi-out-today').textContent = fmtTokens(tokT.output);
544
- document.getElementById('kpi-out-week').textContent = '本周 ' + fmtTokens(tokW.output);
610
+ document.getElementById('kpi-out-week').textContent = lbl + ' ' + fmtTokens(tokR.output);
545
611
 
546
612
  document.getElementById('kpi-cost-today').textContent = t.cost || '–';
547
- document.getElementById('kpi-cost-week').textContent = '本周 ' + (w.cost || '–');
613
+ document.getElementById('kpi-cost-week').textContent = lbl + ' ' + (r.cost || '–');
548
614
 
549
615
  document.getElementById('kpi-fo-today').textContent = fmtNum(t.failovers);
550
- document.getElementById('kpi-fo-week').textContent = '本周 ' + fmtNum(w.failovers);
616
+ document.getElementById('kpi-fo-week').textContent = lbl + ' ' + fmtNum(r.failovers);
551
617
 
552
618
  document.getElementById('kpi-lat-today').textContent = t.avg_duration_ms ? t.avg_duration_ms + 'ms' : '–';
553
- document.getElementById('kpi-lat-week').textContent = '本周 ' + (w.avg_duration_ms ? w.avg_duration_ms + 'ms' : '–');
619
+ document.getElementById('kpi-lat-week').textContent = lbl + ' ' + (r.avg_duration_ms ? r.avg_duration_ms + 'ms' : '–');
554
620
  }
555
621
 
556
622
  // ── 供应商状态 ────────────────────────────────────────────
@@ -578,6 +644,21 @@ function quotaBarColor(pct) {
578
644
  if (pct >= 70) return 'var(--accent-yellow)';
579
645
  return 'var(--accent-green)';
580
646
  }
647
+ function quotaWindowLabel(wh) {
648
+ if (!wh) return '配额';
649
+ const h = parseFloat(wh);
650
+ if (h >= 24) return Math.round(h / 24) + 'd配额';
651
+ return Math.round(h) + 'h配额';
652
+ }
653
+ function renderQuotaBar(qg) {
654
+ if (!qg || qg.usage_percent == null) return '';
655
+ const pct = Math.round(qg.usage_percent);
656
+ const label = quotaWindowLabel(qg.window_hours);
657
+ return `<span class="status-badge ${quotaClass(pct)}">${label} ${pct}%</span>` +
658
+ `<div class="quota-bar-wrap"><div class="quota-bar-bg">` +
659
+ `<div class="quota-bar-fill" style="width:${Math.min(pct,100)}%;background:${quotaBarColor(pct)}"></div>` +
660
+ `</div></div>`;
661
+ }
581
662
 
582
663
  function updateVendorStatus(status) {
583
664
  const tiers = status.tiers || [];
@@ -588,25 +669,13 @@ function updateVendorStatus(status) {
588
669
  }
589
670
  list.innerHTML = tiers.map(tier => {
590
671
  const cb = tier.circuit_breaker || {};
591
- const qg = tier.quota_guard || {};
592
- const wqg = tier.weekly_quota_guard || {};
593
672
  const cbClass = cbStateClass(cb.state);
594
673
  const cbLabel = cbStateLabel(cb.state);
595
- const pct = qg.usage_percent != null ? Math.round(qg.usage_percent) : null;
596
- const wpct = wqg.usage_percent != null ? Math.round(wqg.usage_percent) : null;
597
674
  const initial = (tier.name || '?').charAt(0).toUpperCase();
598
675
 
599
676
  let quotaHTML = '';
600
- if (pct != null) {
601
- quotaHTML += `
602
- <span class="status-badge ${quotaClass(pct)}">日配 ${pct}%</span>
603
- <div class="quota-bar-wrap">
604
- <div class="quota-bar-bg"><div class="quota-bar-fill" style="width:${Math.min(pct,100)}%;background:${quotaBarColor(pct)}"></div></div>
605
- </div>`;
606
- }
607
- if (wpct != null) {
608
- quotaHTML += `<span class="status-badge ${quotaClass(wpct)}">周配 ${wpct}%</span>`;
609
- }
677
+ if (tier.quota_guard) quotaHTML += renderQuotaBar(tier.quota_guard);
678
+ if (tier.weekly_quota_guard) quotaHTML += renderQuotaBar(tier.weekly_quota_guard);
610
679
 
611
680
  const rlInfo = tier.rate_limit || {};
612
681
  const rlHtml = rlInfo.limited ? `<span class="status-badge sb-warn">限速中</span>` : '';
@@ -659,7 +728,10 @@ function buildTimeline(rows) {
659
728
  options: {
660
729
  responsive: true, maintainAspectRatio: false,
661
730
  interaction: { mode: 'index', intersect: false },
662
- plugins: { legend: COMMON_LEGEND },
731
+ plugins: {
732
+ legend: { ...COMMON_LEGEND, onClick: legendOnClick },
733
+ tooltip: { itemSort: (a, b) => (b.raw || 0) - (a.raw || 0) },
734
+ },
663
735
  scales: {
664
736
  x: COMMON_SCALE_X,
665
737
  y: { ...COMMON_SCALE_Y, ticks: { precision: 0 } },
@@ -699,7 +771,7 @@ function buildVendorDist(rows) {
699
771
  options: {
700
772
  responsive: true, maintainAspectRatio: false,
701
773
  plugins: {
702
- legend: COMMON_LEGEND,
774
+ legend: { ...COMMON_LEGEND, onClick: legendOnClick },
703
775
  tooltip: { callbacks: { label: c => ` ${c.label}: ${c.raw.toLocaleString()} 次` } },
704
776
  },
705
777
  },
@@ -748,8 +820,11 @@ function buildTokenTimeline(rows) {
748
820
  responsive: true, maintainAspectRatio: false,
749
821
  interaction: { mode: 'index', intersect: false },
750
822
  plugins: {
751
- legend: COMMON_LEGEND,
752
- tooltip: { callbacks: { label: c => ` ${c.dataset.label}: ${fmtTokens(c.raw)}` } },
823
+ legend: { ...COMMON_LEGEND, onClick: legendOnClick },
824
+ tooltip: {
825
+ itemSort: (a, b) => (b.raw || 0) - (a.raw || 0),
826
+ callbacks: { label: c => ` ${c.dataset.label}: ${fmtTokens(c.raw)}` },
827
+ },
753
828
  },
754
829
  scales: {
755
830
  x: COMMON_SCALE_X,
@@ -811,16 +886,21 @@ function buildModelTokenTimeline(rows) {
811
886
  plugins: {
812
887
  legend: {
813
888
  position: keys.length > 8 ? 'right' : 'bottom',
889
+ onClick: legendOnClick,
814
890
  labels: {
815
891
  ...COMMON_LEGEND.labels,
816
- generateLabels: chart => Chart.defaults.plugins.legend.labels.generateLabels(chart).map(item => {
892
+ generateLabels: chart => {
893
+ const items = COMMON_LEGEND.labels.generateLabels(chart);
817
894
  const maxLen = 32;
818
- if (item.text.length > maxLen) item.text = item.text.slice(0, maxLen) + '…';
819
- return item;
820
- }),
895
+ items.forEach(item => {
896
+ if (item.text.length > maxLen) item.text = item.text.slice(0, maxLen) + '…';
897
+ });
898
+ return items;
899
+ },
821
900
  },
822
901
  },
823
902
  tooltip: {
903
+ itemSort: (a, b) => (b.raw || 0) - (a.raw || 0),
824
904
  callbacks: {
825
905
  label: c => ` ${c.dataset.label}: ${fmtTokens(c.raw)}`,
826
906
  footer: items => {
@@ -842,26 +922,14 @@ function buildModelTokenTimeline(rows) {
842
922
  });
843
923
  }
844
924
 
845
- // ── 故障转移明细表 ────────────────────────────────────────
846
- function updateFtTable(failoverStats) {
847
- const tbody = document.getElementById('ft-tbody');
848
- if (!failoverStats || !failoverStats.length) {
849
- tbody.innerHTML = '<tr><td colspan="3" class="empty"><div class="empty-icon">✅</div>暂无故障转移记录</td></tr>';
850
- return;
851
- }
852
- tbody.innerHTML = failoverStats.map(r => `
853
- <tr>
854
- <td><span class="tag-vendor">${r.failover_from || 'unknown'}</span></td>
855
- <td><span class="tag-vendor">${r.vendor || ''}</span></td>
856
- <td><span style="font-family:'JetBrains Mono',monospace">${fmtNum(r.count)}</span></td>
857
- </tr>`).join('');
858
- }
859
-
860
925
  // ── 时间区间控制 ──────────────────────────────────────────
861
926
  let currentDays = 7;
927
+ let currentRangeLabel = '本周';
862
928
 
863
929
  function setTimeRange(days, btn) {
864
930
  currentDays = days;
931
+ if (days === 7) currentRangeLabel = '本周';
932
+ else if (days === 30) currentRangeLabel = '本月';
865
933
  document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
866
934
  if (btn) btn.classList.add('active');
867
935
  const customEl = document.getElementById('range-custom');
@@ -887,6 +955,7 @@ function applyCustomRange() {
887
955
  const endMs = new Date(e).getTime();
888
956
  if (endMs < startMs) return;
889
957
  currentDays = Math.ceil((endMs - startMs) / 86400000) + 1;
958
+ currentRangeLabel = s + '—' + e;
890
959
  refresh();
891
960
  }
892
961
 
@@ -911,7 +980,7 @@ async function refresh() {
911
980
  try {
912
981
  const days = currentDays > 0 ? currentDays : 7;
913
982
  const [summary, timeline, status] = await Promise.all([
914
- fetchJSON('/api/dashboard/summary'),
983
+ fetchJSON('/api/dashboard/summary?days=' + days),
915
984
  fetchJSON('/api/dashboard/timeline?days=' + days),
916
985
  fetchJSON('/api/status'),
917
986
  ]);
@@ -930,8 +999,6 @@ async function refresh() {
930
999
  buildTokenTimeline(rows);
931
1000
  buildModelTokenTimeline(rows);
932
1001
 
933
- updateFtTable(summary.failover_stats || []);
934
-
935
1002
  document.getElementById('refresh-time').textContent = '上次刷新: ' + now();
936
1003
  } catch (e) {
937
1004
  console.error('Dashboard refresh error:', e);
@@ -943,7 +1010,7 @@ async function refresh() {
943
1010
 
944
1011
  // 页面加载 + 每 30 秒自动刷新
945
1012
  refresh();
946
- setInterval(refresh, 30000);
1013
+ setInterval(refresh, 600000);
947
1014
  </script>
948
1015
  </body>
949
1016
  </html>
@@ -1030,8 +1097,8 @@ def register_dashboard_routes(app: Any) -> None:
1030
1097
  return HTMLResponse(content=_DASHBOARD_HTML)
1031
1098
 
1032
1099
  @app.get("/api/dashboard/summary")
1033
- async def dashboard_summary(request: Request) -> Response:
1034
- """返回 Dashboard 汇总数据(今日 / 本周 / 本月)."""
1100
+ async def dashboard_summary(request: Request, days: int = 7) -> Response:
1101
+ """返回 Dashboard 汇总数据(今日 / 所选区间)."""
1035
1102
  token_logger = getattr(request.app.state, "token_logger", None)
1036
1103
  pricing_table = getattr(request.app.state, "pricing_table", None)
1037
1104
 
@@ -1042,15 +1109,17 @@ def register_dashboard_routes(app: Any) -> None:
1042
1109
  media_type="application/json",
1043
1110
  )
1044
1111
 
1112
+ days = max(1, min(days, 90)) # 限制范围 1~90 天
1113
+
1045
1114
  try:
1046
1115
  # 今日(最近 1 天)
1047
1116
  today_rows = await token_logger.query_usage(period=TimePeriod.DAY, count=1)
1048
- # 本周(最近 7 天)
1049
- week_rows = await token_logger.query_usage(period=TimePeriod.DAY, count=7)
1050
- # 本月(最近 30 天)
1051
- month_rows = await token_logger.query_usage(period=TimePeriod.DAY, count=30)
1052
- # 故障转移(最近 7 天)
1053
- failover_stats = await token_logger.query_failover_stats(days=7)
1117
+ # 所选区间
1118
+ range_rows = await token_logger.query_usage(
1119
+ period=TimePeriod.DAY, count=days
1120
+ )
1121
+ # 故障转移(所选区间)
1122
+ failover_stats = await token_logger.query_failover_stats(days=days)
1054
1123
  except Exception as exc:
1055
1124
  logger.error("dashboard_summary query error: %s", exc, exc_info=True)
1056
1125
  return Response(
@@ -1060,18 +1129,15 @@ def register_dashboard_routes(app: Any) -> None:
1060
1129
  )
1061
1130
 
1062
1131
  today = _sum_rows(today_rows)
1063
- week = _sum_rows(week_rows)
1064
- month = _sum_rows(month_rows)
1132
+ range_stat = _sum_rows(range_rows)
1065
1133
 
1066
1134
  today["cost"] = _compute_cost_str(today_rows, pricing_table)
1067
- week["cost"] = _compute_cost_str(week_rows, pricing_table)
1068
- month["cost"] = _compute_cost_str(month_rows, pricing_table)
1135
+ range_stat["cost"] = _compute_cost_str(range_rows, pricing_table)
1069
1136
 
1070
1137
  result = {
1071
1138
  "version": __version__,
1072
1139
  "today": today,
1073
- "week": week,
1074
- "month": month,
1140
+ "range": range_stat,
1075
1141
  "failover_stats": failover_stats,
1076
1142
  }
1077
1143
  return Response(
@@ -65,7 +65,7 @@ wheels = [
65
65
 
66
66
  [[package]]
67
67
  name = "coding-proxy"
68
- version = "0.2.3a3"
68
+ version = "0.2.3a5"
69
69
  source = { editable = "." }
70
70
  dependencies = [
71
71
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes