coding-proxy 0.2.3a3__tar.gz → 0.2.3a4__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.3a4}/PKG-INFO +1 -1
  2. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/pyproject.toml +1 -1
  3. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/config.default.yaml +1 -1
  4. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/logging/db.py +1 -1
  5. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/executor.py +6 -1
  6. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/quota_guard.py +1 -0
  7. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/dashboard.py +159 -94
  8. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/uv.lock +1 -1
  9. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/.github/workflows/ci.yml +0 -0
  10. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/.github/workflows/coverage.yml +0 -0
  11. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/.github/workflows/release.yml +0 -0
  12. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/.gitignore +0 -0
  13. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/AGENTS.md +0 -0
  14. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/CHANGELOG.md +0 -0
  15. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/CLAUDE.md +0 -0
  16. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/LICENSE +0 -0
  17. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/README.md +0 -0
  18. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/docs/ci-cd.md +0 -0
  19. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/docs/framework.md +0 -0
  20. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/docs/user-guide.md +0 -0
  21. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/docs/zh-CN/README.md +0 -0
  22. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/__init__.py +0 -0
  23. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/__init__.py +0 -0
  24. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/__main__.py +0 -0
  25. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/__init__.py +0 -0
  26. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/providers/__init__.py +0 -0
  27. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/providers/base.py +0 -0
  28. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/providers/github.py +0 -0
  29. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/providers/google.py +0 -0
  30. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/runtime.py +0 -0
  31. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/auth/store.py +0 -0
  32. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/cli/__init__.py +0 -0
  33. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/cli/auth_commands.py +0 -0
  34. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/cli/banner.py +0 -0
  35. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/compat/__init__.py +0 -0
  36. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/compat/canonical.py +0 -0
  37. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/compat/session_store.py +0 -0
  38. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/__init__.py +0 -0
  39. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/auth_schema.py +0 -0
  40. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/loader.py +0 -0
  41. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/resiliency.py +0 -0
  42. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/routing.py +0 -0
  43. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/schema.py +0 -0
  44. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/server.py +0 -0
  45. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/config/vendors.py +0 -0
  46. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/convert/__init__.py +0 -0
  47. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  48. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  49. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  50. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  51. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  52. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/logging/__init__.py +0 -0
  53. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/logging/formatters.py +0 -0
  54. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/logging/stats.py +0 -0
  55. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/__init__.py +0 -0
  56. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/auth.py +0 -0
  57. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/compat.py +0 -0
  58. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/constants.py +0 -0
  59. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/pricing.py +0 -0
  60. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/token.py +0 -0
  61. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/model/vendor.py +0 -0
  62. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/pricing.py +0 -0
  63. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/__init__.py +0 -0
  64. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  65. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/error_classifier.py +0 -0
  66. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/model_mapper.py +0 -0
  67. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/rate_limit.py +0 -0
  68. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/retry.py +0 -0
  69. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/router.py +0 -0
  70. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/session_manager.py +0 -0
  71. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/tier.py +0 -0
  72. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/usage_parser.py +0 -0
  73. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/routing/usage_recorder.py +0 -0
  74. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/__init__.py +0 -0
  75. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/app.py +0 -0
  76. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/factory.py +0 -0
  77. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/request_normalizer.py +0 -0
  78. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/responses.py +0 -0
  79. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/server/routes.py +0 -0
  80. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/streaming/__init__.py +0 -0
  81. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  82. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/__init__.py +0 -0
  83. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/alibaba.py +0 -0
  84. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/anthropic.py +0 -0
  85. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/antigravity.py +0 -0
  86. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/base.py +0 -0
  87. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/copilot.py +0 -0
  88. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/copilot_models.py +0 -0
  89. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  90. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  91. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/doubao.py +0 -0
  92. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/kimi.py +0 -0
  93. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/minimax.py +0 -0
  94. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/mixins.py +0 -0
  95. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  96. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/token_manager.py +0 -0
  97. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/xiaomi.py +0 -0
  98. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/src/coding/proxy/vendors/zhipu.py +0 -0
  99. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/__init__.py +0 -0
  100. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_antigravity.py +0 -0
  101. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_app_routes.py +0 -0
  102. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_auto_login.py +0 -0
  103. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_banner.py +0 -0
  104. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_circuit_breaker.py +0 -0
  105. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_cli_usage.py +0 -0
  106. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_compat.py +0 -0
  107. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_config_init.py +0 -0
  108. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_config_loader.py +0 -0
  109. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_convert_request.py +0 -0
  110. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_convert_response.py +0 -0
  111. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_convert_sse.py +0 -0
  112. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_copilot.py +0 -0
  113. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_copilot_convert_request.py +0 -0
  114. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_copilot_convert_response.py +0 -0
  115. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_copilot_models.py +0 -0
  116. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_copilot_urls.py +0 -0
  117. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_currency.py +0 -0
  118. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_error_classifier.py +0 -0
  119. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_logging_dual_write.py +0 -0
  120. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_mixins.py +0 -0
  121. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_auth.py +0 -0
  122. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_compat.py +0 -0
  123. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_constants.py +0 -0
  124. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_mapper.py +0 -0
  125. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_pricing.py +0 -0
  126. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_token.py +0 -0
  127. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_model_vendor.py +0 -0
  128. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_native_vendors.py +0 -0
  129. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_parse_usage.py +0 -0
  130. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_pricing.py +0 -0
  131. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_quota_guard.py +0 -0
  132. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_rate_limit.py +0 -0
  133. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_request_normalizer.py +0 -0
  134. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_router_chain.py +0 -0
  135. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_router_executor.py +0 -0
  136. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_runtime_reauth.py +0 -0
  137. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_schema.py +0 -0
  138. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_streaming_anthropic_compat.py +0 -0
  139. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_tier.py +0 -0
  140. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_tiers_config.py +0 -0
  141. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_time_range.py +0 -0
  142. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_token_logger.py +0 -0
  143. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_token_manager.py +0 -0
  144. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_types.py +0 -0
  145. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_vendor_streaming.py +0 -0
  146. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/tests/test_vendors.py +0 -0
  147. {coding_proxy-0.2.3a3 → coding_proxy-0.2.3a4}/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.3a4
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.3a4"
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
  // ── 工具函数 ──────────────────────────────────────────────
@@ -505,9 +510,68 @@ Chart.defaults.font.size = 11;
505
510
 
506
511
  const COMMON_SCALE_X = { grid: { display: false }, ticks: { maxTicksLimit: 10 } };
507
512
  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 } } };
513
+ const COMMON_LEGEND = {
514
+ position: 'bottom',
515
+ labels: {
516
+ boxWidth: 8,
517
+ padding: 14,
518
+ usePointStyle: true,
519
+ pointStyle: 'circle',
520
+ pointStyleWidth: 8,
521
+ font: { size: 11 },
522
+ generateLabels: chart => {
523
+ const items = Chart.defaults.plugins.legend.labels.generateLabels(chart);
524
+ items.forEach(item => { item.pointStyle = 'circle'; item.lineWidth = 0; });
525
+ return items;
526
+ },
527
+ },
528
+ };
509
529
  const COMMON_LINE_DATASET = { tension: .35, pointRadius: 0, pointHoverRadius: 5, borderWidth: 2 };
510
530
 
531
+ // ── Legend 点击交互:单击=仅选该项,Ctrl/Meta+单击=多选追加,Shift+单击=排除 ──
532
+ function legendOnClick(e, legendItem, legend) {
533
+ const chart = legend.chart;
534
+ const isShift = e.native.shiftKey;
535
+ const isCtrl = e.native.ctrlKey || e.native.metaKey;
536
+ if (chart.config.type === 'doughnut' || chart.config.type === 'pie') {
537
+ const idx = legendItem.index;
538
+ const dataLen = chart.data.labels.length;
539
+ if (isShift) {
540
+ chart.toggleDataVisibility(idx);
541
+ } else if (isCtrl) {
542
+ if (!chart.getDataVisibility(idx)) chart.toggleDataVisibility(idx);
543
+ } else {
544
+ const allOthersHidden = [...Array(dataLen).keys()].filter(i => i !== idx).every(i => !chart.getDataVisibility(i));
545
+ if (allOthersHidden) {
546
+ for (let i = 0; i < dataLen; i++) { if (!chart.getDataVisibility(i)) chart.toggleDataVisibility(i); }
547
+ } else {
548
+ for (let i = 0; i < dataLen; i++) {
549
+ const vis = chart.getDataVisibility(i);
550
+ if (i === idx && !vis) chart.toggleDataVisibility(i);
551
+ if (i !== idx && vis) chart.toggleDataVisibility(i);
552
+ }
553
+ }
554
+ }
555
+ } else {
556
+ const idx = legendItem.datasetIndex;
557
+ const datasets = chart.data.datasets;
558
+ if (isShift) {
559
+ const meta = chart.getDatasetMeta(idx);
560
+ meta.hidden = !meta.hidden;
561
+ } else if (isCtrl) {
562
+ chart.getDatasetMeta(idx).hidden = false;
563
+ } else {
564
+ const allOthersHidden = datasets.every((_, i) => i === idx || !!chart.getDatasetMeta(i).hidden);
565
+ if (allOthersHidden) {
566
+ datasets.forEach((_, i) => { chart.getDatasetMeta(i).hidden = false; });
567
+ } else {
568
+ datasets.forEach((_, i) => { chart.getDatasetMeta(i).hidden = (i !== idx); });
569
+ }
570
+ }
571
+ }
572
+ chart.update();
573
+ }
574
+
511
575
  // ── 图表实例 ──────────────────────────────────────────────
512
576
  let chartTimeline = null;
513
577
  let chartVendorDist = null;
@@ -529,28 +593,29 @@ async function fetchJSON(url) {
529
593
 
530
594
  // ── KPI 更新 ──────────────────────────────────────────────
531
595
  function updateKPI(summary) {
532
- const t = summary.today, w = summary.week;
596
+ const t = summary.today, r = summary.range;
597
+ const lbl = currentRangeLabel;
533
598
 
534
599
  document.getElementById('kpi-req-today').textContent = fmtNum(t.requests);
535
- document.getElementById('kpi-req-week').textContent = '本周 ' + fmtNum(w.requests);
600
+ document.getElementById('kpi-req-week').textContent = lbl + ' ' + fmtNum(r.requests);
536
601
 
537
- const tokT = t.tokens, tokW = w.tokens;
602
+ const tokT = t.tokens, tokR = r.tokens;
538
603
  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;
604
+ const totalR = tokR.input + tokR.output + tokR.cache_creation + tokR.cache_read;
540
605
  document.getElementById('kpi-tok-today').textContent = fmtTokens(totalT);
541
- document.getElementById('kpi-tok-week').textContent = '本周 ' + fmtTokens(totalW);
606
+ document.getElementById('kpi-tok-week').textContent = lbl + ' ' + fmtTokens(totalR);
542
607
 
543
608
  document.getElementById('kpi-out-today').textContent = fmtTokens(tokT.output);
544
- document.getElementById('kpi-out-week').textContent = '本周 ' + fmtTokens(tokW.output);
609
+ document.getElementById('kpi-out-week').textContent = lbl + ' ' + fmtTokens(tokR.output);
545
610
 
546
611
  document.getElementById('kpi-cost-today').textContent = t.cost || '–';
547
- document.getElementById('kpi-cost-week').textContent = '本周 ' + (w.cost || '–');
612
+ document.getElementById('kpi-cost-week').textContent = lbl + ' ' + (r.cost || '–');
548
613
 
549
614
  document.getElementById('kpi-fo-today').textContent = fmtNum(t.failovers);
550
- document.getElementById('kpi-fo-week').textContent = '本周 ' + fmtNum(w.failovers);
615
+ document.getElementById('kpi-fo-week').textContent = lbl + ' ' + fmtNum(r.failovers);
551
616
 
552
617
  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' : '–');
618
+ document.getElementById('kpi-lat-week').textContent = lbl + ' ' + (r.avg_duration_ms ? r.avg_duration_ms + 'ms' : '–');
554
619
  }
555
620
 
556
621
  // ── 供应商状态 ────────────────────────────────────────────
@@ -578,6 +643,21 @@ function quotaBarColor(pct) {
578
643
  if (pct >= 70) return 'var(--accent-yellow)';
579
644
  return 'var(--accent-green)';
580
645
  }
646
+ function quotaWindowLabel(wh) {
647
+ if (!wh) return '配额';
648
+ const h = parseFloat(wh);
649
+ if (h >= 24) return Math.round(h / 24) + 'd配额';
650
+ return Math.round(h) + 'h配额';
651
+ }
652
+ function renderQuotaBar(qg) {
653
+ if (!qg || qg.usage_percent == null) return '';
654
+ const pct = Math.round(qg.usage_percent);
655
+ const label = quotaWindowLabel(qg.window_hours);
656
+ return `<span class="status-badge ${quotaClass(pct)}">${label} ${pct}%</span>` +
657
+ `<div class="quota-bar-wrap"><div class="quota-bar-bg">` +
658
+ `<div class="quota-bar-fill" style="width:${Math.min(pct,100)}%;background:${quotaBarColor(pct)}"></div>` +
659
+ `</div></div>`;
660
+ }
581
661
 
582
662
  function updateVendorStatus(status) {
583
663
  const tiers = status.tiers || [];
@@ -588,25 +668,13 @@ function updateVendorStatus(status) {
588
668
  }
589
669
  list.innerHTML = tiers.map(tier => {
590
670
  const cb = tier.circuit_breaker || {};
591
- const qg = tier.quota_guard || {};
592
- const wqg = tier.weekly_quota_guard || {};
593
671
  const cbClass = cbStateClass(cb.state);
594
672
  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
673
  const initial = (tier.name || '?').charAt(0).toUpperCase();
598
674
 
599
675
  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
- }
676
+ if (tier.quota_guard) quotaHTML += renderQuotaBar(tier.quota_guard);
677
+ if (tier.weekly_quota_guard) quotaHTML += renderQuotaBar(tier.weekly_quota_guard);
610
678
 
611
679
  const rlInfo = tier.rate_limit || {};
612
680
  const rlHtml = rlInfo.limited ? `<span class="status-badge sb-warn">限速中</span>` : '';
@@ -659,7 +727,10 @@ function buildTimeline(rows) {
659
727
  options: {
660
728
  responsive: true, maintainAspectRatio: false,
661
729
  interaction: { mode: 'index', intersect: false },
662
- plugins: { legend: COMMON_LEGEND },
730
+ plugins: {
731
+ legend: { ...COMMON_LEGEND, onClick: legendOnClick },
732
+ tooltip: { itemSort: (a, b) => (b.raw || 0) - (a.raw || 0) },
733
+ },
663
734
  scales: {
664
735
  x: COMMON_SCALE_X,
665
736
  y: { ...COMMON_SCALE_Y, ticks: { precision: 0 } },
@@ -699,7 +770,7 @@ function buildVendorDist(rows) {
699
770
  options: {
700
771
  responsive: true, maintainAspectRatio: false,
701
772
  plugins: {
702
- legend: COMMON_LEGEND,
773
+ legend: { ...COMMON_LEGEND, onClick: legendOnClick },
703
774
  tooltip: { callbacks: { label: c => ` ${c.label}: ${c.raw.toLocaleString()} 次` } },
704
775
  },
705
776
  },
@@ -748,8 +819,11 @@ function buildTokenTimeline(rows) {
748
819
  responsive: true, maintainAspectRatio: false,
749
820
  interaction: { mode: 'index', intersect: false },
750
821
  plugins: {
751
- legend: COMMON_LEGEND,
752
- tooltip: { callbacks: { label: c => ` ${c.dataset.label}: ${fmtTokens(c.raw)}` } },
822
+ legend: { ...COMMON_LEGEND, onClick: legendOnClick },
823
+ tooltip: {
824
+ itemSort: (a, b) => (b.raw || 0) - (a.raw || 0),
825
+ callbacks: { label: c => ` ${c.dataset.label}: ${fmtTokens(c.raw)}` },
826
+ },
753
827
  },
754
828
  scales: {
755
829
  x: COMMON_SCALE_X,
@@ -811,16 +885,21 @@ function buildModelTokenTimeline(rows) {
811
885
  plugins: {
812
886
  legend: {
813
887
  position: keys.length > 8 ? 'right' : 'bottom',
888
+ onClick: legendOnClick,
814
889
  labels: {
815
890
  ...COMMON_LEGEND.labels,
816
- generateLabels: chart => Chart.defaults.plugins.legend.labels.generateLabels(chart).map(item => {
891
+ generateLabels: chart => {
892
+ const items = COMMON_LEGEND.labels.generateLabels(chart);
817
893
  const maxLen = 32;
818
- if (item.text.length > maxLen) item.text = item.text.slice(0, maxLen) + '…';
819
- return item;
820
- }),
894
+ items.forEach(item => {
895
+ if (item.text.length > maxLen) item.text = item.text.slice(0, maxLen) + '…';
896
+ });
897
+ return items;
898
+ },
821
899
  },
822
900
  },
823
901
  tooltip: {
902
+ itemSort: (a, b) => (b.raw || 0) - (a.raw || 0),
824
903
  callbacks: {
825
904
  label: c => ` ${c.dataset.label}: ${fmtTokens(c.raw)}`,
826
905
  footer: items => {
@@ -842,26 +921,14 @@ function buildModelTokenTimeline(rows) {
842
921
  });
843
922
  }
844
923
 
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
924
  // ── 时间区间控制 ──────────────────────────────────────────
861
925
  let currentDays = 7;
926
+ let currentRangeLabel = '本周';
862
927
 
863
928
  function setTimeRange(days, btn) {
864
929
  currentDays = days;
930
+ if (days === 7) currentRangeLabel = '本周';
931
+ else if (days === 30) currentRangeLabel = '本月';
865
932
  document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
866
933
  if (btn) btn.classList.add('active');
867
934
  const customEl = document.getElementById('range-custom');
@@ -887,6 +954,7 @@ function applyCustomRange() {
887
954
  const endMs = new Date(e).getTime();
888
955
  if (endMs < startMs) return;
889
956
  currentDays = Math.ceil((endMs - startMs) / 86400000) + 1;
957
+ currentRangeLabel = s + '—' + e;
890
958
  refresh();
891
959
  }
892
960
 
@@ -911,7 +979,7 @@ async function refresh() {
911
979
  try {
912
980
  const days = currentDays > 0 ? currentDays : 7;
913
981
  const [summary, timeline, status] = await Promise.all([
914
- fetchJSON('/api/dashboard/summary'),
982
+ fetchJSON('/api/dashboard/summary?days=' + days),
915
983
  fetchJSON('/api/dashboard/timeline?days=' + days),
916
984
  fetchJSON('/api/status'),
917
985
  ]);
@@ -930,8 +998,6 @@ async function refresh() {
930
998
  buildTokenTimeline(rows);
931
999
  buildModelTokenTimeline(rows);
932
1000
 
933
- updateFtTable(summary.failover_stats || []);
934
-
935
1001
  document.getElementById('refresh-time').textContent = '上次刷新: ' + now();
936
1002
  } catch (e) {
937
1003
  console.error('Dashboard refresh error:', e);
@@ -1030,8 +1096,8 @@ def register_dashboard_routes(app: Any) -> None:
1030
1096
  return HTMLResponse(content=_DASHBOARD_HTML)
1031
1097
 
1032
1098
  @app.get("/api/dashboard/summary")
1033
- async def dashboard_summary(request: Request) -> Response:
1034
- """返回 Dashboard 汇总数据(今日 / 本周 / 本月)."""
1099
+ async def dashboard_summary(request: Request, days: int = 7) -> Response:
1100
+ """返回 Dashboard 汇总数据(今日 / 所选区间)."""
1035
1101
  token_logger = getattr(request.app.state, "token_logger", None)
1036
1102
  pricing_table = getattr(request.app.state, "pricing_table", None)
1037
1103
 
@@ -1042,15 +1108,17 @@ def register_dashboard_routes(app: Any) -> None:
1042
1108
  media_type="application/json",
1043
1109
  )
1044
1110
 
1111
+ days = max(1, min(days, 90)) # 限制范围 1~90 天
1112
+
1045
1113
  try:
1046
1114
  # 今日(最近 1 天)
1047
1115
  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)
1116
+ # 所选区间
1117
+ range_rows = await token_logger.query_usage(
1118
+ period=TimePeriod.DAY, count=days
1119
+ )
1120
+ # 故障转移(所选区间)
1121
+ failover_stats = await token_logger.query_failover_stats(days=days)
1054
1122
  except Exception as exc:
1055
1123
  logger.error("dashboard_summary query error: %s", exc, exc_info=True)
1056
1124
  return Response(
@@ -1060,18 +1128,15 @@ def register_dashboard_routes(app: Any) -> None:
1060
1128
  )
1061
1129
 
1062
1130
  today = _sum_rows(today_rows)
1063
- week = _sum_rows(week_rows)
1064
- month = _sum_rows(month_rows)
1131
+ range_stat = _sum_rows(range_rows)
1065
1132
 
1066
1133
  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)
1134
+ range_stat["cost"] = _compute_cost_str(range_rows, pricing_table)
1069
1135
 
1070
1136
  result = {
1071
1137
  "version": __version__,
1072
1138
  "today": today,
1073
- "week": week,
1074
- "month": month,
1139
+ "range": range_stat,
1075
1140
  "failover_stats": failover_stats,
1076
1141
  }
1077
1142
  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.3a4"
69
69
  source = { editable = "." }
70
70
  dependencies = [
71
71
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes