coding-proxy 0.4.1a9__tar.gz → 0.4.1a11__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 (194) hide show
  1. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/CHANGELOG.md +2 -0
  2. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/PKG-INFO +1 -1
  3. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/agents/issue.md +8 -4
  4. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/arch/config-reference.md +34 -6
  5. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/pyproject.toml +1 -1
  6. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/config.default.yaml +8 -0
  7. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/routing.py +10 -3
  8. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/schema.py +2 -0
  9. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/vendors.py +17 -1
  10. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/vendor_channels.py +4 -2
  11. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/executor.py +99 -28
  12. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/server/dashboard.py +163 -0
  13. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/server/factory.py +10 -6
  14. coding_proxy-0.4.1a11/src/coding/proxy/vendors/concurrency.py +83 -0
  15. coding_proxy-0.4.1a11/src/coding/proxy/vendors/zhipu.py +332 -0
  16. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_schema.py +2 -1
  17. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_vendors.py +25 -2
  18. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_zhipu.py +44 -3
  19. coding_proxy-0.4.1a11/tests/test_zhipu_concurrency.py +557 -0
  20. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/uv.lock +1 -1
  21. coding_proxy-0.4.1a9/src/coding/proxy/vendors/zhipu.py +0 -188
  22. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/.github/workflows/ci.yml +0 -0
  23. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/.github/workflows/coverage.yml +0 -0
  24. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/.github/workflows/release.yml +0 -0
  25. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/.gitignore +0 -0
  26. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/.pre-commit-config.yaml +0 -0
  27. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/AGENTS.md +0 -0
  28. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/CLAUDE.md +0 -0
  29. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/LICENSE +0 -0
  30. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/README.md +0 -0
  31. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/assets/dashboard-v0.4.0.png +0 -0
  32. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/assets/session-v0.4.0.png +0 -0
  33. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/agents/browser-validation.md +0 -0
  34. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/agents/knowledge-map.md +0 -0
  35. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/agents/reference-specifications.md +0 -0
  36. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/arch/convert.md +0 -0
  37. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/arch/design-patterns.md +0 -0
  38. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/arch/routing.md +0 -0
  39. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/arch/testing.md +0 -0
  40. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/arch/vendors.md +0 -0
  41. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/framework.md +0 -0
  42. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/guide/api-reference.md +0 -0
  43. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/guide/cli-reference.md +0 -0
  44. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/guide/dashboard.md +0 -0
  45. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/guide/monitoring.md +0 -0
  46. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/guide/quickstart.md +0 -0
  47. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/guide/vendors.md +0 -0
  48. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/ops/ci-cd.md +0 -0
  49. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/user-guide.md +0 -0
  50. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/docs/zh-CN/README.md +0 -0
  51. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/__init__.py +0 -0
  52. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/__init__.py +0 -0
  53. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/__main__.py +0 -0
  54. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/__init__.py +0 -0
  55. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/providers/__init__.py +0 -0
  56. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/providers/base.py +0 -0
  57. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/providers/github.py +0 -0
  58. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/providers/google.py +0 -0
  59. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/runtime.py +0 -0
  60. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/auth/store.py +0 -0
  61. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/cli/__init__.py +0 -0
  62. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/cli/auth_commands.py +0 -0
  63. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/cli/banner.py +0 -0
  64. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/compat/__init__.py +0 -0
  65. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/compat/canonical.py +0 -0
  66. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/compat/session_store.py +0 -0
  67. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/__init__.py +0 -0
  68. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/auth_schema.py +0 -0
  69. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/loader.py +0 -0
  70. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/resiliency.py +0 -0
  71. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/server.py +0 -0
  72. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/config/session_policy.py +0 -0
  73. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/__init__.py +0 -0
  74. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  75. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  76. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  77. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  78. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  79. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/logging/__init__.py +0 -0
  80. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/logging/db.py +0 -0
  81. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/logging/formatters.py +0 -0
  82. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/logging/stats.py +0 -0
  83. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/__init__.py +0 -0
  84. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/auth.py +0 -0
  85. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/compat.py +0 -0
  86. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/constants.py +0 -0
  87. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/pricing.py +0 -0
  88. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/token.py +0 -0
  89. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/model/vendor.py +0 -0
  90. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/__init__.py +0 -0
  91. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/config.py +0 -0
  92. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  93. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  94. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  95. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  96. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/handler.py +0 -0
  97. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/operation.py +0 -0
  98. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/routes.py +0 -0
  99. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/native_api/usage_registry.py +0 -0
  100. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/pricing.py +0 -0
  101. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/__init__.py +0 -0
  102. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  103. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/error_classifier.py +0 -0
  104. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/model_mapper.py +0 -0
  105. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/quota_guard.py +0 -0
  106. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/rate_limit.py +0 -0
  107. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/retry.py +0 -0
  108. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/router.py +0 -0
  109. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/session_manager.py +0 -0
  110. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/session_policy.py +0 -0
  111. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/tier.py +0 -0
  112. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/usage_parser.py +0 -0
  113. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/routing/usage_recorder.py +0 -0
  114. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/server/__init__.py +0 -0
  115. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/server/app.py +0 -0
  116. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/server/responses.py +0 -0
  117. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/server/routes.py +0 -0
  118. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/streaming/__init__.py +0 -0
  119. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  120. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/__init__.py +0 -0
  121. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/alibaba.py +0 -0
  122. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/anthropic.py +0 -0
  123. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/antigravity.py +0 -0
  124. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/base.py +0 -0
  125. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/copilot.py +0 -0
  126. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/copilot_models.py +0 -0
  127. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  128. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  129. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/doubao.py +0 -0
  130. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/kimi.py +0 -0
  131. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/minimax.py +0 -0
  132. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/mixins.py +0 -0
  133. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  134. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/token_manager.py +0 -0
  135. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/src/coding/proxy/vendors/xiaomi.py +0 -0
  136. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/__init__.py +0 -0
  137. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/e2e/__init__.py +0 -0
  138. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/e2e/conftest.py +0 -0
  139. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/e2e/test_e2e_http.py +0 -0
  140. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/e2e/test_e2e_token.py +0 -0
  141. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/e2e/test_e2e_vendor.py +0 -0
  142. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_antigravity.py +0 -0
  143. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_app_routes.py +0 -0
  144. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_auto_login.py +0 -0
  145. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_banner.py +0 -0
  146. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_circuit_breaker.py +0 -0
  147. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_cli_usage.py +0 -0
  148. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_compat.py +0 -0
  149. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_config_init.py +0 -0
  150. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_config_loader.py +0 -0
  151. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_convert_request.py +0 -0
  152. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_convert_response.py +0 -0
  153. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_convert_sse.py +0 -0
  154. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_copilot.py +0 -0
  155. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_copilot_convert_request.py +0 -0
  156. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_copilot_convert_response.py +0 -0
  157. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_copilot_models.py +0 -0
  158. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_copilot_urls.py +0 -0
  159. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_currency.py +0 -0
  160. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_error_classifier.py +0 -0
  161. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_logging_dual_write.py +0 -0
  162. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_mixins.py +0 -0
  163. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_auth.py +0 -0
  164. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_compat.py +0 -0
  165. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_constants.py +0 -0
  166. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_mapper.py +0 -0
  167. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_pricing.py +0 -0
  168. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_token.py +0 -0
  169. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_model_vendor.py +0 -0
  170. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_native_api_base_url_override.py +0 -0
  171. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_native_api_extractors.py +0 -0
  172. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_native_api_handler.py +0 -0
  173. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_native_api_operation.py +0 -0
  174. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_native_api_routes.py +0 -0
  175. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_native_vendors.py +0 -0
  176. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_parse_usage.py +0 -0
  177. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_parse_usage_gemini.py +0 -0
  178. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_pricing.py +0 -0
  179. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_quota_guard.py +0 -0
  180. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_rate_limit.py +0 -0
  181. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_router_chain.py +0 -0
  182. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_router_executor.py +0 -0
  183. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_runtime_reauth.py +0 -0
  184. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_session_aware.py +0 -0
  185. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_streaming_anthropic_compat.py +0 -0
  186. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_tier.py +0 -0
  187. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_tiers_config.py +0 -0
  188. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_time_range.py +0 -0
  189. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_token_logger.py +0 -0
  190. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_token_logger_native_columns.py +0 -0
  191. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_token_manager.py +0 -0
  192. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_types.py +0 -0
  193. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_vendor_channels.py +0 -0
  194. {coding_proxy-0.4.1a9 → coding_proxy-0.4.1a11}/tests/test_vendor_streaming.py +0 -0
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ - feat(zhipu): 新增每模型并发限制(默认 3,可通过 `vendors[zhipu].concurrency` 配置),基于 `asyncio.Semaphore` 实现 FIFO 公平排队,流式与非流式共用同一槽位,与 429 重试机制兼容。
8
+
7
9
  ## [v0.4.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.4.0) — 2026-05-01
8
10
 
9
11
  > [!IMPORTANT]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.4.1a9
3
+ Version: 0.4.1a11
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
@@ -253,15 +253,19 @@ INFO Tier anthropic message succeeded (took over from failed tier: zhipu)
253
253
 
254
254
  `is_semantic_rejection` 检测到 zhipu 返回 `invalid_request_error + 1210` 含「API 调用参数有误」中文标记,判定为语义拒绝,跳过下一层 tier。1210 是智谱官方错误码,[官方文档](https://docs.bigmodel.cn/cn/api/api-code) 定义为「参数格式/类型不符规范」(区别于 1213「必需字段缺失」、1214「字段参数非法」)。
255
255
 
256
- **根因(仍在收集证据)**
256
+ **根因(已定位,修复中)**
257
257
 
258
- PR #244 的初版诊断字段仅覆盖 `thinking / thinking_blocks / cache_control / model / messages`,但 2026-05-25 17:26 后的诊断日志显示失败请求**均不含**上述任何字段。说明真正祸根在更细粒度的参数(system / tools / max_tokens / sampling / metadata / content_types / body_size 等)。
258
+ PR #247 (Step 1 v2) 部署后,2026-05-26 16:30–16:31 的诊断日志显示 8 次连续拒绝**全部携带 `thinking={"type": "adaptive"}`**(Anthropic Claude 4.x 新增的参数类型),而同一时段其他会话的请求持续成功。之前 curl 测试仅验证了 `{"type": "enabled"}`,未覆盖 `adaptive` 类型。GLM 可能不支持此特定类型值,导致 [1210] 参数校验失败。
259
259
 
260
260
  **处理方式(分阶段)**
261
261
 
262
262
  - **Step 1(PR #244,已合并)**:在 `executor.py::_build_semantic_rejection_diagnostic` 中输出 thinking / cache_control 相关字段 — 但证据反转,覆盖不足以定位真因。
263
- - **Step 1 v2(本次)**:扩展诊断函数覆盖 `system_kind|blocks(+cc)` / `tools` / `tool_choice` / 采样参数 / `stream` / `metadata_keys` / `content_types` / `body_bytes` 等维度。所有项「仅存在时输出」以控制日志噪声。配套 14 个单元测试(`TestBuildSemanticRejectionDiagnostic`)覆盖各字段组合。
264
- - **Step 2(待定)**:依据扩展诊断日志的新证据,定位具体祸根参数后再施修复(候选路径:`ZhipuVendor._prepare_request` 参数剥离 / 调用现有 `normalize_for_zhipu` / pre-validation 警告)。
263
+ - **Step 1 v2(PR #247,已合并)**:扩展诊断函数覆盖 `system_kind|blocks(+cc)` / `tools` / `tool_choice` / 采样参数 / `stream` / `metadata_keys` / `content_types` / `body_bytes` 等维度。所有项「仅存在时输出」以控制日志噪声。配套 14 个单元测试(`TestBuildSemanticRejectionDiagnostic`)覆盖各字段组合。
264
+ - **Step 2(进行中)**:基于 Step 1 v2 的日志证据,在 `ZhipuVendor._prepare_request` 中实现 **兼容转换**(而非移除):
265
+ - `thinking.type="adaptive"` → `{"type": "enabled", "budget_tokens": 16000}`(保留 thinking 能力)
266
+ - 新增 `_build_zhipu_request_snapshot` 诊断快照,同时覆盖成功/失败请求,建立可对比证据链
267
+ - 扩展语义拒绝日志的错误体截断限制(200 → 500 字符),保留完整字段级诊断
268
+ - `metadata` 暂不处理(待进一步诊断确认兼容性)
265
269
 
266
270
  **后续防范**
267
271
 
@@ -89,12 +89,13 @@ flowchart TD
89
89
 
90
90
  ## 5. VendorConfig 弹性字段
91
91
 
92
- | 字段 | 类型 | 默认值 | 说明 |
93
- | -------------------- | -------------- | -------------------- | --------------------------- |
94
- | `circuit_breaker` | config \| None | `None` | 熔断器配置(None = 终端层) |
95
- | `retry` | config | `RetryConfig()` | 重试策略配置 |
96
- | `quota_guard` | config | `QuotaGuardConfig()` | 日度配额守卫配置 |
97
- | `weekly_quota_guard` | config | `QuotaGuardConfig()` | 周度配额守卫配置 |
92
+ | 字段 | 类型 | 默认值 | 说明 |
93
+ | -------------------- | -------------- | -------------------- | ----------------------------------- |
94
+ | `circuit_breaker` | config \| None | `None` | 熔断器配置(None = 终端层) |
95
+ | `retry` | config | `RetryConfig()` | 重试策略配置 |
96
+ | `quota_guard` | config | `QuotaGuardConfig()` | 日度配额守卫配置 |
97
+ | `weekly_quota_guard` | config | `QuotaGuardConfig()` | 周度配额守卫配置 |
98
+ | `concurrency` | config \| None | `None` | `[zhipu]` 每模型并发限制(详见 5.5) |
98
99
 
99
100
  <a id="elastic-params"></a>
100
101
 
@@ -143,6 +144,33 @@ flowchart TD
143
144
  | `error_types` | list[str] | `["rate_limit_error", "overloaded_error", "api_error"]` |
144
145
  | `error_message_patterns` | list[str] | `["quota", "limit exceeded", "usage cap", "capacity", "internal network failure"]` |
145
146
 
147
+ ### 5.5 ZhipuConcurrencyConfig — Zhipu 每模型并发参数
148
+
149
+ 仅对 `vendor: zhipu` 生效,基于 `asyncio.Semaphore` 实现 FIFO 公平排队。
150
+
151
+ | 字段 | 类型 | 默认值 | 说明 |
152
+ | --------- | -------------- | ------ | -------------------------------------------------------------------------------- |
153
+ | `default` | int | `3` | 全局默认并行度(适用于所有未在 `models` 中显式覆盖的模型);取值范围 `[1, 20]` |
154
+ | `models` | map[str → int] | `{}` | 按映射后模型名(如 `glm-5v-turbo` / `glm-5.1` / `glm-4.5-air`)自定义并行度上限 |
155
+
156
+ YAML 示例:
157
+
158
+ ```yaml
159
+ - vendor: zhipu
160
+ concurrency:
161
+ default: 3
162
+ models:
163
+ glm-5v-turbo: 5
164
+ glm-5.1: 2
165
+ ```
166
+
167
+ 行为语义:
168
+
169
+ - 信号量按**映射后模型名**键控,与上游真实承载模型对齐;流式与非流式请求共用同一槽位。
170
+ - 槽位满时新请求按 FIFO 顺序排队,直到任一在途请求释放槽位才被唤醒。
171
+ - 429 重试期间持续占用槽位(重试视为同一请求的延续)。
172
+ - 顶层 `concurrency` 字段缺省为 `None` → 转发至 `ZhipuConfig` 时回退默认值 `default=3`;如需完全关闭限流,可在 `ZhipuConfig` 构造层显式置 `null`(一般无需操作)。
173
+
146
174
  ---
147
175
 
148
176
  ## 6. 供应商专属字段
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.4.1a9"
3
+ version = "0.4.1a11"
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"
@@ -119,6 +119,14 @@ vendors:
119
119
  window_hours: 24.0
120
120
  threshold_percent: 95.0
121
121
  probe_interval_seconds: 300
122
+ # 每模型并发限制:默认 3 个并行请求;超出则按 FIFO 排队等待
123
+ # 可通过 models 字段覆盖单个模型的限制(如 glm-5.1: 5)
124
+ concurrency:
125
+ default: 3
126
+ # models:
127
+ # glm-5v-turbo: 3
128
+ # glm-5.1: 3
129
+ # glm-4.5-air: 3
122
130
 
123
131
  # Vendor 4: MiniMax(默认禁用,需手动启用并添加到 tiers)
124
132
  - vendor: minimax
@@ -9,6 +9,7 @@ from typing import Annotated, Any, Literal
9
9
  from pydantic import BaseModel, BeforeValidator, Field, PrivateAttr, model_validator
10
10
 
11
11
  from .resiliency import CircuitBreakerConfig, QuotaGuardConfig, RetryConfig
12
+ from .vendors import ZhipuConcurrencyConfig
12
13
 
13
14
  # ── 价格字段解析($ / ¥ 前缀支持) ──────────────────────────
14
15
 
@@ -64,13 +65,13 @@ _NATIVE_ANTHROPIC_FIELDS: frozenset[str] = frozenset(
64
65
  "api_key",
65
66
  }
66
67
  )
67
- # 向后兼容别名
68
- _ZHIPU_FIELDS = _NATIVE_ANTHROPIC_FIELDS
68
+ # Zhipu 独占字段:在通用 api_key 基础上增加每模型并发限制
69
+ _ZHIPU_FIELDS: frozenset[str] = _NATIVE_ANTHROPIC_FIELDS | frozenset({"concurrency"})
69
70
 
70
71
  _VENDOR_EXCLUSIVE_FIELDS: dict[str, frozenset[str]] = {
71
72
  "copilot": _COPILOT_FIELDS,
72
73
  "antigravity": _ANTIGRAVITY_FIELDS,
73
- "zhipu": _NATIVE_ANTHROPIC_FIELDS,
74
+ "zhipu": _ZHIPU_FIELDS,
74
75
  "minimax": _NATIVE_ANTHROPIC_FIELDS,
75
76
  "kimi": _NATIVE_ANTHROPIC_FIELDS,
76
77
  "doubao": _NATIVE_ANTHROPIC_FIELDS,
@@ -285,6 +286,12 @@ class VendorConfig(BaseModel):
285
286
  quota_guard: QuotaGuardConfig = Field(default_factory=QuotaGuardConfig)
286
287
  weekly_quota_guard: QuotaGuardConfig = Field(default_factory=QuotaGuardConfig)
287
288
 
289
+ # ── Zhipu 专属:每模型并发限制 ───────────────────────────
290
+ concurrency: ZhipuConcurrencyConfig | None = Field(
291
+ default=None,
292
+ description="[zhipu] 每模型并发限制;None 表示不限并发",
293
+ )
294
+
288
295
  @model_validator(mode="after")
289
296
  def _warn_irrelevant_fields(self) -> VendorConfig:
290
297
  """对非当前 vendor 类型的非空专属字段发出 warning."""
@@ -54,6 +54,7 @@ from .vendors import ( # noqa: F401
54
54
  KimiConfig,
55
55
  MinimaxConfig,
56
56
  XiaomiConfig,
57
+ ZhipuConcurrencyConfig,
57
58
  ZhipuConfig,
58
59
  )
59
60
 
@@ -318,6 +319,7 @@ __all__ = [
318
319
  "CopilotConfig",
319
320
  "AntigravityConfig",
320
321
  "ZhipuConfig",
322
+ "ZhipuConcurrencyConfig",
321
323
  # resiliency
322
324
  "CircuitBreakerConfig",
323
325
  "RetryConfig",
@@ -2,7 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ZhipuConcurrencyConfig(BaseModel):
9
+ """Zhipu 每模型并发限制配置."""
10
+
11
+ default: int = Field(default=3, ge=1, le=20, description="全局默认并行度")
12
+ models: dict[str, int] = Field(
13
+ default_factory=dict,
14
+ description="按映射后模型名自定义并行度(覆盖 default)",
15
+ )
16
+
17
+ def get_limit(self, model: str) -> int:
18
+ """获取指定模型的并行度限制."""
19
+ return self.models.get(model, self.default)
6
20
 
7
21
 
8
22
  class AnthropicConfig(BaseModel):
@@ -48,6 +62,7 @@ class ZhipuConfig(BaseModel):
48
62
  base_url: str = "https://open.bigmodel.cn/api/anthropic"
49
63
  api_key: str = ""
50
64
  timeout_ms: int = 3000000
65
+ concurrency: ZhipuConcurrencyConfig = Field(default_factory=ZhipuConcurrencyConfig)
51
66
 
52
67
 
53
68
  class MinimaxConfig(BaseModel):
@@ -100,6 +115,7 @@ __all__ = [
100
115
  "CopilotConfig",
101
116
  "AntigravityConfig",
102
117
  "ZhipuConfig",
118
+ "ZhipuConcurrencyConfig",
103
119
  "MinimaxConfig",
104
120
  "KimiConfig",
105
121
  "DoubaoConfig",
@@ -369,8 +369,10 @@ def _strip_cache_control(body: dict[str, Any]) -> int:
369
369
 
370
370
  # ── zhipu 共享清洗函数 ──────────────────────────────────────────
371
371
 
372
- # 跨供应商转换时主动剥离的顶层参数(首选 tier 场景由 _prepare_request 原样透传,
373
- # GLM 原生支持 thinking / 静默忽略 cache_control 和 reasoning_effort,不会触发 400)。
372
+ # 跨供应商转换时主动剥离的顶层参数。
373
+ # 首选 tier 场景的 thinking.type=adaptive 兼容转换由
374
+ # ZhipuVendor._prepare_request 处理(转换为 enabled + budget,保留功能),
375
+ # 此处仅负责 failover 路径的全量剥离(跨供应商 thinking signature 失效)。
374
376
  _ZHIPU_UNSUPPORTED_PARAMS: frozenset[str] = frozenset(
375
377
  {"thinking", "extended_thinking", "reasoning_effort"}
376
378
  )
@@ -129,50 +129,116 @@ def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
129
129
 
130
130
  在 semantic rejection 日志中附加请求体的可疑参数快照,
131
131
  用于定位供应商参数校验失败的具体祸根参数。
132
+
133
+ 覆盖范围:
134
+ * 模型 / messages 数(baseline)
135
+ * thinking 系列顶层参数 + history thinking_blocks 数
136
+ * system 形态(string / blocks,含 cache_control 计数)
137
+ * tools 数量 + tool_choice 形态
138
+ * 采样参数(max_tokens / temperature / top_p / top_k / stop_sequences)
139
+ * stream / metadata 形态
140
+ * cache_control 存在性
141
+ * messages.content 类型分布
142
+ * 请求体大小估算(json.dumps 字节数)
132
143
  """
133
144
  parts: list[str] = []
134
- # 顶层不兼容参数
145
+
146
+ # ── 模型 + 消息数(baseline,始终输出)──
147
+ parts.append(f"model={body.get('model', 'N/A')}")
148
+ parts.append(f"messages={len(body.get('messages', []))}")
149
+
150
+ # ── 顶层 thinking 系列参数 ──
135
151
  for key in ("thinking", "extended_thinking", "reasoning_effort"):
136
152
  if key in body:
137
153
  val = body[key]
138
154
  parts.append(f"{key}={val!r:.80}")
139
- # 会话历史中的 thinking blocks
155
+
156
+ # ── system 形态 ──
157
+ system = body.get("system")
158
+ if isinstance(system, str):
159
+ parts.append(f"system_kind=string(len={len(system)})")
160
+ elif isinstance(system, list):
161
+ cc_count = sum(
162
+ 1 for item in system if isinstance(item, dict) and "cache_control" in item
163
+ )
164
+ if cc_count:
165
+ parts.append(f"system_blocks={len(system)},cc={cc_count}")
166
+ else:
167
+ parts.append(f"system_blocks={len(system)}")
168
+
169
+ # ── tools 与 tool_choice ──
170
+ tools = body.get("tools")
171
+ if isinstance(tools, list):
172
+ parts.append(f"tools={len(tools)}")
173
+ tool_choice = body.get("tool_choice")
174
+ if tool_choice is not None:
175
+ parts.append(f"tool_choice={tool_choice!r:.60}")
176
+
177
+ # ── 采样参数(仅存在时输出)──
178
+ for key in ("max_tokens", "temperature", "top_p", "top_k"):
179
+ if key in body:
180
+ parts.append(f"{key}={body[key]!r:.40}")
181
+ stop_sequences = body.get("stop_sequences")
182
+ if isinstance(stop_sequences, list) and stop_sequences:
183
+ parts.append(f"stop_sequences={len(stop_sequences)}")
184
+
185
+ # ── stream / metadata ──
186
+ if "stream" in body:
187
+ parts.append(f"stream={body['stream']}")
188
+ metadata = body.get("metadata")
189
+ if isinstance(metadata, dict) and metadata:
190
+ parts.append(f"metadata_keys={len(metadata)}")
191
+
192
+ # ── 会话历史中的 thinking blocks 与 content_types 分布 ──
140
193
  thinking_count = 0
194
+ content_type_counts: dict[str, int] = {}
141
195
  for msg in body.get("messages", []):
142
196
  content = msg.get("content")
197
+ if isinstance(content, str):
198
+ content_type_counts["string"] = content_type_counts.get("string", 0) + 1
199
+ continue
143
200
  if not isinstance(content, list):
144
201
  continue
145
202
  for block in content:
146
- if isinstance(block, dict) and block.get("type") in (
147
- "thinking",
148
- "redacted_thinking",
149
- ):
203
+ if not isinstance(block, dict):
204
+ continue
205
+ btype = block.get("type")
206
+ if isinstance(btype, str):
207
+ content_type_counts[btype] = content_type_counts.get(btype, 0) + 1
208
+ if btype in ("thinking", "redacted_thinking"):
150
209
  thinking_count += 1
151
210
  if thinking_count:
152
211
  parts.append(f"thinking_blocks_in_history={thinking_count}")
153
- # cache_control 存在检测
212
+ if content_type_counts:
213
+ type_repr = ",".join(f"{k}:{v}" for k, v in sorted(content_type_counts.items()))
214
+ parts.append(f"content_types={{{type_repr}}}")
215
+
216
+ # ── cache_control 存在检测(messages / tools,不含 system 因已单独统计)──
154
217
  has_cc = False
155
- for section in (
156
- body.get("system", []) if isinstance(body.get("system"), list) else [],
157
- *(
158
- m.get("content", [])
159
- for m in body.get("messages", [])
160
- if isinstance(m.get("content"), list)
161
- ),
162
- body.get("tools", []),
163
- ):
164
- if isinstance(section, list):
165
- for item in section:
166
- if isinstance(item, dict) and "cache_control" in item:
167
- has_cc = True
168
- break
218
+ sections: list[Any] = []
219
+ for m in body.get("messages", []):
220
+ if isinstance(m.get("content"), list):
221
+ sections.append(m["content"])
222
+ if isinstance(body.get("tools"), list):
223
+ sections.append(body["tools"])
224
+ for section in sections:
225
+ for item in section:
226
+ if isinstance(item, dict) and "cache_control" in item:
227
+ has_cc = True
228
+ break
169
229
  if has_cc:
170
230
  break
171
231
  if has_cc:
172
232
  parts.append("cache_control_fields=present")
173
- # 模型 + 消息数
174
- parts.append(f"model={body.get('model', 'N/A')}")
175
- parts.append(f"messages={len(body.get('messages', []))}")
233
+
234
+ # ── 请求体大小估算 ──
235
+ try:
236
+ body_bytes = len(json.dumps(body, ensure_ascii=False).encode("utf-8"))
237
+ parts.append(f"body_bytes={body_bytes}")
238
+ except (TypeError, ValueError):
239
+ # 极少数情况下 body 含非可序列化对象,跳过
240
+ pass
241
+
176
242
  return f" [{', '.join(parts)}]" if parts else ""
177
243
 
178
244
 
@@ -860,12 +926,15 @@ class _RouteExecutor:
860
926
 
861
927
  if not is_last and is_semantic:
862
928
  diagnostic = _build_semantic_rejection_diagnostic(body)
929
+ # zhipu 等供应商的错误体含字段级诊断(如 [1210] 错误码 + request_id),
930
+ # 500 字符足以覆盖完整错误体,避免截断丢失关键细节
931
+ err_msg = (resp.error_message or "N/A")[:500]
863
932
  logger.warning(
864
933
  "Tier %s semantic rejection (type=%s, msg=%s)%s, "
865
934
  "trying next tier without recording failure",
866
935
  tier.name,
867
936
  resp.error_type or resp.status_code,
868
- (resp.error_message or "N/A")[:200],
937
+ err_msg,
869
938
  diagnostic,
870
939
  )
871
940
  failed_tier_name = tier.name
@@ -1100,14 +1169,16 @@ class _RouteExecutor:
1100
1169
  if semantic_rejection and not is_last:
1101
1170
  if request_body is not None:
1102
1171
  diagnostic = _build_semantic_rejection_diagnostic(request_body)
1172
+ stream_err_msg = (
1173
+ error.get("message") if isinstance(error, dict) else "N/A"
1174
+ )
1175
+ # 扩展至 500 字符以保留完整字段级诊断信息
1103
1176
  logger.warning(
1104
1177
  "Tier %s stream semantic rejection (type=%s, msg=%s)%s, "
1105
1178
  "trying next tier without recording failure",
1106
1179
  tier.name,
1107
1180
  error.get("type") if isinstance(error, dict) else None,
1108
- (error.get("message") if isinstance(error, dict) else "N/A")[
1109
- :200
1110
- ],
1181
+ stream_err_msg[:500],
1111
1182
  diagnostic,
1112
1183
  )
1113
1184
  return True, tier.name, exc
@@ -557,6 +557,89 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
557
557
  .tab-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
558
558
  .tab-pane { display: none; }
559
559
  .tab-pane.active { display: block; }
560
+
561
+ /* ── Model Calling 实时状态 ────────────────────────── */
562
+ .model-calling-card {
563
+ margin-bottom: 5px;
564
+ }
565
+ .mc-empty {
566
+ text-align: center;
567
+ color: var(--text-muted);
568
+ padding: 16px 0;
569
+ font-size: 13px;
570
+ }
571
+ .mc-grid {
572
+ display: grid;
573
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
574
+ gap: 8px;
575
+ }
576
+ .mc-model-row {
577
+ display: flex;
578
+ align-items: center;
579
+ gap: 10px;
580
+ padding: 8px 12px;
581
+ background: var(--bg-secondary);
582
+ border-radius: var(--radius-sm);
583
+ border: 1px solid var(--border-subtle);
584
+ }
585
+ .mc-model-name {
586
+ font-family: 'JetBrains Mono', monospace;
587
+ font-size: 12px;
588
+ color: var(--text-primary);
589
+ min-width: 140px;
590
+ white-space: nowrap;
591
+ overflow: hidden;
592
+ text-overflow: ellipsis;
593
+ }
594
+ .mc-bar-wrap {
595
+ flex: 1;
596
+ min-width: 60px;
597
+ height: 6px;
598
+ background: rgba(255,255,255,.06);
599
+ border-radius: 3px;
600
+ overflow: hidden;
601
+ }
602
+ .mc-bar-fill {
603
+ height: 100%;
604
+ border-radius: 3px;
605
+ transition: width .3s ease, background .3s ease;
606
+ }
607
+ .mc-bar-fill.mc-low { background: var(--accent-green); }
608
+ .mc-bar-fill.mc-mid { background: var(--accent-yellow); }
609
+ .mc-bar-fill.mc-high { background: var(--accent-red); }
610
+ .mc-stats {
611
+ display: flex;
612
+ align-items: center;
613
+ gap: 6px;
614
+ font-size: 11px;
615
+ font-family: 'JetBrains Mono', monospace;
616
+ color: var(--text-muted);
617
+ white-space: nowrap;
618
+ }
619
+ .mc-badge {
620
+ display: inline-flex;
621
+ align-items: center;
622
+ padding: 1px 6px;
623
+ border-radius: 4px;
624
+ font-size: 10px;
625
+ font-weight: 600;
626
+ font-family: 'JetBrains Mono', monospace;
627
+ }
628
+ .mc-badge-pending {
629
+ background: rgba(251,146,60,.15);
630
+ color: #fb923c;
631
+ }
632
+ .mc-badge-active {
633
+ background: rgba(74,222,128,.12);
634
+ color: #4ade80;
635
+ }
636
+ .mc-vendor-tag {
637
+ font-size: 10px;
638
+ color: var(--text-muted);
639
+ background: rgba(255,255,255,.06);
640
+ padding: 1px 6px;
641
+ border-radius: 3px;
642
+ }
560
643
  </style>
561
644
  </head>
562
645
  <body>
@@ -626,6 +709,14 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
626
709
  </div>
627
710
  </div>
628
711
 
712
+ <!-- Model Calling 实时状态 -->
713
+ <div class="card model-calling-card" id="model-calling-card">
714
+ <div class="card-title">📡 Model Calling 实时状态</div>
715
+ <div class="model-calling-wrap" id="model-calling-wrap">
716
+ <div class="mc-empty">加载中…</div>
717
+ </div>
718
+ </div>
719
+
629
720
  <!-- 供应商状态 + 请求量趋势折线图 -->
630
721
  <div class="charts-grid">
631
722
  <div class="card">
@@ -1134,6 +1225,74 @@ function updateVendorStatus(status) {
1134
1225
  }).join('');
1135
1226
  }
1136
1227
 
1228
+ // ── Model Calling 实时状态 ────────────────────────────────
1229
+ function updateModelCalling(status) {
1230
+ var wrap = document.getElementById('model-calling-wrap');
1231
+ if (!wrap) return;
1232
+ var tiers = status.tiers || [];
1233
+
1234
+ // 收集所有带 concurrency 诊断的模型
1235
+ var models = [];
1236
+ for (var i = 0; i < tiers.length; i++) {
1237
+ var tier = tiers[i];
1238
+ var diag = tier.diagnostics || {};
1239
+ var conc = diag.concurrency;
1240
+ if (!conc) continue;
1241
+ var names = Object.keys(conc);
1242
+ for (var j = 0; j < names.length; j++) {
1243
+ var model = names[j];
1244
+ var d = conc[model];
1245
+ models.push({
1246
+ vendor: tier.name,
1247
+ model: model,
1248
+ limit: d.limit || 0,
1249
+ in_use: d.in_use || 0,
1250
+ available: d.available || 0,
1251
+ pending: d.pending || 0,
1252
+ });
1253
+ }
1254
+ }
1255
+
1256
+ if (!models.length) {
1257
+ wrap.innerHTML = '<div class="mc-empty">无活跃模型调用</div>';
1258
+ return;
1259
+ }
1260
+
1261
+ var html = '<div class="mc-grid">';
1262
+ for (var k = 0; k < models.length; k++) {
1263
+ var m = models[k];
1264
+ var pct = m.limit > 0 ? Math.round((m.in_use / m.limit) * 100) : 0;
1265
+ var barClass = pct <= 50 ? 'mc-low' : (pct <= 80 ? 'mc-mid' : 'mc-high');
1266
+
1267
+ html += '<div class="mc-model-row">'
1268
+ + '<span class="mc-model-name">' + escapeHtml(m.vendor + '/' + m.model) + '</span>'
1269
+ + '<div class="mc-bar-wrap"><div class="mc-bar-fill ' + barClass + '" style="width:' + pct + '%"></div></div>'
1270
+ + '<div class="mc-stats">'
1271
+ + '<span class="mc-badge mc-badge-active">' + m.in_use + '/' + m.limit + '</span>'
1272
+ + (m.pending > 0 ? '<span class="mc-badge mc-badge-pending">⏳ ' + m.pending + '</span>' : '')
1273
+ + '</div>'
1274
+ + '</div>';
1275
+ }
1276
+ html += '</div>';
1277
+ wrap.innerHTML = html;
1278
+ }
1279
+
1280
+ // Model Calling 独立短间隔轮询
1281
+ var _mcTimer = null;
1282
+ function startModelCallingPoll() {
1283
+ stopModelCallingPoll();
1284
+ function tick() {
1285
+ fetchJSON('/api/status').then(function(status) {
1286
+ updateModelCalling(status);
1287
+ }).catch(function() {});
1288
+ }
1289
+ tick();
1290
+ _mcTimer = setInterval(tick, 5000);
1291
+ }
1292
+ function stopModelCallingPoll() {
1293
+ if (_mcTimer) { clearInterval(_mcTimer); _mcTimer = null; }
1294
+ }
1295
+
1137
1296
  // ── 按 tiers 顺序排序 vendor 列表 ─────────────────────────
1138
1297
  function sortByTierOrder(vendors, tierOrder) {
1139
1298
  if (!tierOrder || !tierOrder.length) return vendors.sort();
@@ -1713,6 +1872,7 @@ async function refreshOverview() {
1713
1872
 
1714
1873
  updateKPI(summary);
1715
1874
  updateVendorStatus(status);
1875
+ updateModelCalling(status);
1716
1876
  updateChartTitles(days);
1717
1877
 
1718
1878
  const rows = timeline.rows || [];
@@ -1788,6 +1948,8 @@ function switchTab(name) {
1788
1948
  currentTab = name;
1789
1949
  applyTabState(name);
1790
1950
  syncTabUrl(name);
1951
+ // Model Calling 轮询随页签切换启停
1952
+ if (name === 'overview') { startModelCallingPoll(); } else { stopModelCallingPoll(); }
1791
1953
  refresh();
1792
1954
  }
1793
1955
 
@@ -1807,6 +1969,7 @@ function switchTab(name) {
1807
1969
  }).catch(function(){});
1808
1970
  refresh(); // 仅加载初始页签的数据
1809
1971
  setInterval(refresh, 600000); // 每 10 分钟刷新当前页签
1972
+ if (initial === 'overview') startModelCallingPoll();
1810
1973
  })();
1811
1974
  </script>
1812
1975
  </body>
@@ -156,13 +156,17 @@ def _create_vendor_from_config(
156
156
  cfg = _resolve_antigravity_credentials(cfg, token_store)
157
157
  return AntigravityVendor(cfg, failover_cfg, mapper)
158
158
  case "zhipu":
159
- cfg = ZhipuConfig(
160
- enabled=vendor_cfg.enabled,
161
- base_url=vendor_cfg.base_url
159
+ zhipu_kwargs: dict[str, Any] = {
160
+ "enabled": vendor_cfg.enabled,
161
+ "base_url": vendor_cfg.base_url
162
162
  or "https://open.bigmodel.cn/api/anthropic",
163
- api_key=vendor_cfg.api_key,
164
- timeout_ms=vendor_cfg.timeout_ms,
165
- )
163
+ "api_key": vendor_cfg.api_key,
164
+ "timeout_ms": vendor_cfg.timeout_ms,
165
+ }
166
+ # 仅当显式配置了 concurrency 时转发,否则使用 ZhipuConfig 默认值
167
+ if vendor_cfg.concurrency is not None:
168
+ zhipu_kwargs["concurrency"] = vendor_cfg.concurrency
169
+ cfg = ZhipuConfig(**zhipu_kwargs)
166
170
  return ZhipuVendor(cfg, mapper, failover_cfg)
167
171
  case "minimax":
168
172
  cfg = MinimaxConfig(