coding-proxy 0.5.1a6__tar.gz → 0.5.1a9__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 (196) hide show
  1. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/PKG-INFO +1 -1
  2. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/arch/design-patterns.md +2 -0
  3. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/pyproject.toml +1 -1
  4. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/config.default.yaml +53 -35
  5. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/schema.py +2 -1
  6. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/session_policy.py +24 -0
  7. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/executor.py +50 -1
  8. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/router.py +3 -0
  9. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/server/app.py +1 -0
  10. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/zhipu.py +42 -34
  11. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_router_executor.py +290 -0
  12. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_zhipu.py +181 -0
  13. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/uv.lock +1 -1
  14. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/.github/workflows/ci.yml +0 -0
  15. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/.github/workflows/coverage.yml +0 -0
  16. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/.github/workflows/release.yml +0 -0
  17. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/.gitignore +0 -0
  18. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/.pre-commit-config.yaml +0 -0
  19. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/AGENTS.md +0 -0
  20. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/CHANGELOG.md +0 -0
  21. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/CLAUDE.md +0 -0
  22. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/LICENSE +0 -0
  23. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/README.md +0 -0
  24. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/assets/dashboard-v0.4.0.png +0 -0
  25. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/assets/model-calling-v0.5.0.png +0 -0
  26. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/assets/session-v0.4.0.png +0 -0
  27. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/.agents/browser-validation.md +0 -0
  28. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/.agents/issue.md +0 -0
  29. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/.agents/knowledge-map.md +0 -0
  30. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/.agents/reference-specifications.md +0 -0
  31. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/arch/config-reference.md +0 -0
  32. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/arch/convert.md +0 -0
  33. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/arch/routing.md +0 -0
  34. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/arch/testing.md +0 -0
  35. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/arch/vendors.md +0 -0
  36. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/framework.md +0 -0
  37. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/guide/api-reference.md +0 -0
  38. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/guide/cli-reference.md +0 -0
  39. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/guide/dashboard.md +0 -0
  40. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/guide/monitoring.md +0 -0
  41. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/guide/quickstart.md +0 -0
  42. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/guide/vendors.md +0 -0
  43. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/ops/ci-cd.md +0 -0
  44. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/user-guide.md +0 -0
  45. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/docs/zh-CN/README.md +0 -0
  46. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/__init__.py +0 -0
  47. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/__init__.py +0 -0
  48. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/__main__.py +0 -0
  49. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/__init__.py +0 -0
  50. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/__init__.py +0 -0
  51. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/base.py +0 -0
  52. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/github.py +0 -0
  53. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/google.py +0 -0
  54. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/runtime.py +0 -0
  55. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/store.py +0 -0
  56. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/__init__.py +0 -0
  57. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/auth_commands.py +0 -0
  58. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/banner.py +0 -0
  59. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/__init__.py +0 -0
  60. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/canonical.py +0 -0
  61. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/session_store.py +0 -0
  62. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/__init__.py +0 -0
  63. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/auth_schema.py +0 -0
  64. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/loader.py +0 -0
  65. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/resiliency.py +0 -0
  66. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/routing.py +0 -0
  67. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/server.py +0 -0
  68. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/config/vendors.py +0 -0
  69. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/__init__.py +0 -0
  70. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  71. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  72. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  73. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  74. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  75. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/vendor_channels.py +0 -0
  76. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/__init__.py +0 -0
  77. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/db.py +0 -0
  78. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/formatters.py +0 -0
  79. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/stats.py +0 -0
  80. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/__init__.py +0 -0
  81. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/auth.py +0 -0
  82. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/compat.py +0 -0
  83. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/constants.py +0 -0
  84. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/pricing.py +0 -0
  85. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/token.py +0 -0
  86. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/model/vendor.py +0 -0
  87. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/__init__.py +0 -0
  88. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/config.py +0 -0
  89. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  90. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  91. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  92. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  93. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/handler.py +0 -0
  94. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/operation.py +0 -0
  95. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/routes.py +0 -0
  96. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/usage_registry.py +0 -0
  97. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/pricing.py +0 -0
  98. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/__init__.py +0 -0
  99. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  100. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/error_classifier.py +0 -0
  101. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/model_mapper.py +0 -0
  102. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/quota_guard.py +0 -0
  103. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/rate_limit.py +0 -0
  104. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/retry.py +0 -0
  105. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/session_manager.py +0 -0
  106. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/session_policy.py +0 -0
  107. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/tier.py +0 -0
  108. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/usage_parser.py +0 -0
  109. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/usage_recorder.py +0 -0
  110. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/server/__init__.py +0 -0
  111. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/server/dashboard.py +0 -0
  112. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/server/factory.py +0 -0
  113. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/server/responses.py +0 -0
  114. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/server/routes.py +0 -0
  115. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/streaming/__init__.py +0 -0
  116. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  117. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/__init__.py +0 -0
  118. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/alibaba.py +0 -0
  119. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/anthropic.py +0 -0
  120. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/antigravity.py +0 -0
  121. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/base.py +0 -0
  122. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/concurrency.py +0 -0
  123. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot.py +0 -0
  124. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_models.py +0 -0
  125. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  126. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  127. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/doubao.py +0 -0
  128. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/kimi.py +0 -0
  129. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/minimax.py +0 -0
  130. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/mixins.py +0 -0
  131. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  132. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/token_manager.py +0 -0
  133. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/xiaomi.py +0 -0
  134. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/__init__.py +0 -0
  135. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/e2e/__init__.py +0 -0
  136. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/e2e/conftest.py +0 -0
  137. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_http.py +0 -0
  138. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_token.py +0 -0
  139. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_vendor.py +0 -0
  140. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_antigravity.py +0 -0
  141. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_app_routes.py +0 -0
  142. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_auto_login.py +0 -0
  143. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_banner.py +0 -0
  144. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_circuit_breaker.py +0 -0
  145. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_cli_usage.py +0 -0
  146. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_compat.py +0 -0
  147. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_concurrency_monitor.py +0 -0
  148. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_config_init.py +0 -0
  149. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_config_loader.py +0 -0
  150. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_convert_request.py +0 -0
  151. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_convert_response.py +0 -0
  152. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_convert_sse.py +0 -0
  153. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_copilot.py +0 -0
  154. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_copilot_convert_request.py +0 -0
  155. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_copilot_convert_response.py +0 -0
  156. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_copilot_models.py +0 -0
  157. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_copilot_urls.py +0 -0
  158. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_currency.py +0 -0
  159. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_error_classifier.py +0 -0
  160. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_executor_in_flight_tracking.py +0 -0
  161. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_logging_dual_write.py +0 -0
  162. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_mixins.py +0 -0
  163. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_auth.py +0 -0
  164. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_compat.py +0 -0
  165. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_constants.py +0 -0
  166. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_mapper.py +0 -0
  167. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_pricing.py +0 -0
  168. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_token.py +0 -0
  169. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_model_vendor.py +0 -0
  170. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_native_api_base_url_override.py +0 -0
  171. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_native_api_extractors.py +0 -0
  172. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_native_api_handler.py +0 -0
  173. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_native_api_operation.py +0 -0
  174. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_native_api_routes.py +0 -0
  175. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_native_vendors.py +0 -0
  176. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_parse_usage.py +0 -0
  177. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_parse_usage_gemini.py +0 -0
  178. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_pricing.py +0 -0
  179. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_quota_guard.py +0 -0
  180. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_rate_limit.py +0 -0
  181. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_router_chain.py +0 -0
  182. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_runtime_reauth.py +0 -0
  183. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_schema.py +0 -0
  184. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_session_aware.py +0 -0
  185. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_streaming_anthropic_compat.py +0 -0
  186. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_tier.py +0 -0
  187. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_tiers_config.py +0 -0
  188. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_time_range.py +0 -0
  189. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_token_logger.py +0 -0
  190. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_token_logger_native_columns.py +0 -0
  191. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_token_manager.py +0 -0
  192. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_types.py +0 -0
  193. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_vendor_channels.py +0 -0
  194. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_vendor_streaming.py +0 -0
  195. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_vendors.py +0 -0
  196. {coding_proxy-0.5.1a6 → coding_proxy-0.5.1a9}/tests/test_zhipu_concurrency.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.5.1a6
3
+ Version: 0.5.1a9
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
@@ -541,6 +541,8 @@ $$
541
541
 
542
542
  > **参数默认值**:参见 [配置参考 -- RetryConfig](./config-reference.md#elastic-params)
543
543
 
544
+ **Vendor 级应用(Zhipu 429/529 内部退避重试)**:[`vendors/zhipu.py`](../../src/coding/proxy/vendors/zhipu.py) 复用 `RetryConfig` / `calculate_delay()`,在 `send_message` / `send_message_stream` 内对 **429 Rate Limit(限流)** 与 **529 Overloaded(并发过载)** 两类服务端瞬态过载信号做就地指数退避重试(共用 `_BACKOFF_RETRY_STATUS = {429, 529}` 作单一事实源,max=5、1s→2s→4s→8s、Full Jitter,优先尊重 server `retry-after`)。耗尽重试后将状态码原样返回,交由上层 `should_trigger_failover`(§3.6 VendorTier)与 [CircuitBreaker](#circuit-breaker) 处理,从而降低 failover 频率。
545
+
544
546
  ---
545
547
 
546
548
  ## 3.13 Rate Limit Deadline Tracking(速率限制截止追踪)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.5.1a6"
3
+ version = "0.5.1a9"
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"
@@ -228,51 +228,51 @@ failover:
228
228
  # === 模型映射 ===
229
229
  model_mapping:
230
230
  # GitHub Copilot 可用模型(通过 /api/copilot/models 诊断获取)
231
- - pattern: "claude-sonnet-.*"
232
- vendors: ["copilot"]
233
- target: "claude-sonnet-4.6"
234
- is_regex: true
235
231
  - pattern: "claude-opus-.*"
236
232
  vendors: ["copilot"]
237
233
  target: "claude-opus-4.6"
238
234
  is_regex: true
235
+ - pattern: "claude-sonnet-.*"
236
+ vendors: ["copilot"]
237
+ target: "claude-sonnet-4.6"
238
+ is_regex: true
239
239
  - pattern: "claude-haiku-.*"
240
240
  vendors: ["copilot"]
241
241
  target: "claude-haiku-4.5"
242
242
  is_regex: true
243
243
  # Google Antigravity 模型映射
244
- - pattern: "claude-sonnet-.*"
245
- vendors: ["antigravity"]
246
- target: "Claude Sonnet 4.6"
247
- is_regex: true
248
244
  - pattern: "claude-opus-.*"
249
245
  vendors: ["antigravity"]
250
246
  target: "Claude Opus 4.6 Thinking"
251
247
  is_regex: true
248
+ - pattern: "claude-sonnet-.*"
249
+ vendors: ["antigravity"]
250
+ target: "Claude Sonnet 4.6"
251
+ is_regex: true
252
252
  - pattern: "claude-haiku-.*"
253
253
  vendors: ["antigravity"]
254
254
  target: "Gemini 3 Flash"
255
255
  is_regex: true
256
256
  # 智谱 GLM 映射(官方原生 Anthropic 兼容端点)
257
257
  # glm-5v-turbo 完整承载 Claude Code 满血能力(tool calling / thinking / 多模态)
258
- - pattern: "claude-sonnet-.*"
258
+ - pattern: "claude-opus-.*"
259
259
  vendors: ["zhipu"]
260
- target: "glm-5v-turbo"
260
+ target: "glm-5.2"
261
261
  is_regex: true
262
- - pattern: "claude-opus-.*"
262
+ - pattern: "claude-sonnet-.*"
263
263
  vendors: ["zhipu"]
264
264
  target: "glm-5.1"
265
265
  is_regex: true
266
266
  - pattern: "claude-haiku-.*"
267
267
  vendors: ["zhipu"]
268
- target: "glm-4.5-air"
268
+ target: "glm-5.1"
269
269
  is_regex: true
270
270
  # MiniMax 模型映射(原生 Anthropic 兼容端点)
271
- - pattern: "claude-sonnet-.*"
271
+ - pattern: "claude-opus-.*"
272
272
  vendors: ["minimax"]
273
273
  target: "minimax-m2.7"
274
274
  is_regex: true
275
- - pattern: "claude-opus-.*"
275
+ - pattern: "claude-sonnet-.*"
276
276
  vendors: ["minimax"]
277
277
  target: "minimax-m2.7"
278
278
  is_regex: true
@@ -281,11 +281,11 @@ model_mapping:
281
281
  target: "minimax-m2.7"
282
282
  is_regex: true
283
283
  # 阿里 Qwen 模型映射(原生 Anthropic 兼容端点)
284
- - pattern: "claude-sonnet-.*"
284
+ - pattern: "claude-opus-.*"
285
285
  vendors: ["alibaba"]
286
286
  target: "qwen3.6-plus"
287
287
  is_regex: true
288
- - pattern: "claude-opus-.*"
288
+ - pattern: "claude-sonnet-.*"
289
289
  vendors: ["alibaba"]
290
290
  target: "qwen3.6-plus"
291
291
  is_regex: true
@@ -294,11 +294,11 @@ model_mapping:
294
294
  target: "qwen3.6-plus"
295
295
  is_regex: true
296
296
  # 小米 MiMo 模型映射(原生 Anthropic 兼容端点)
297
- - pattern: "claude-sonnet-.*"
297
+ - pattern: "claude-opus-.*"
298
298
  vendors: ["xiaomi"]
299
299
  target: "mimo-v2-pro"
300
300
  is_regex: true
301
- - pattern: "claude-opus-.*"
301
+ - pattern: "claude-sonnet-.*"
302
302
  vendors: ["xiaomi"]
303
303
  target: "mimo-v2-pro"
304
304
  is_regex: true
@@ -307,11 +307,11 @@ model_mapping:
307
307
  target: "mimo-v2-pro"
308
308
  is_regex: true
309
309
  # Kimi 模型映射(原生 Anthropic 兼容端点)
310
- - pattern: "claude-sonnet-.*"
310
+ - pattern: "claude-opus-.*"
311
311
  vendors: ["kimi"]
312
312
  target: "kimi-k2.5"
313
313
  is_regex: true
314
- - pattern: "claude-opus-.*"
314
+ - pattern: "claude-sonnet-.*"
315
315
  vendors: ["kimi"]
316
316
  target: "kimi-k2.5"
317
317
  is_regex: true
@@ -339,6 +339,12 @@ model_mapping:
339
339
  # 未配置定价的模型在统计中 Cost 列显示 "-"
340
340
  pricing:
341
341
  # ── Anthropic Claude ──
342
+ - vendor: anthropic
343
+ model: claude-fable-5
344
+ input_cost_per_mtok: $10.0
345
+ output_cost_per_mtok: $50.0
346
+ cache_write_cost_per_mtok: $12.50
347
+ cache_read_cost_per_mtok: $1.00
342
348
  - vendor: anthropic
343
349
  model: claude-opus-4-8
344
350
  input_cost_per_mtok: $5.0
@@ -527,20 +533,10 @@ pricing:
527
533
  cache_read_cost_per_mtok: $0.05
528
534
  # ── 智谱 GLM ──
529
535
  - vendor: zhipu
530
- model: glm-4.5-air # 待区分长短上下文定价
531
- input_cost_per_mtok: ¥0.80
532
- output_cost_per_mtok: ¥2.00
533
- cache_read_cost_per_mtok: ¥0.16
534
- - vendor: zhipu
535
- model: glm-4.7 # 待区分长短上下文定价
536
- input_cost_per_mtok: ¥2.00
537
- output_cost_per_mtok: ¥8.00
538
- cache_read_cost_per_mtok: ¥0.40
539
- - vendor: zhipu
540
- model: glm-5v-turbo # 待区分长短上下文定价
541
- input_cost_per_mtok: ¥5.00
542
- output_cost_per_mtok: ¥22.00
543
- cache_read_cost_per_mtok: ¥1.20
536
+ model: glm-5.2 # 待区分长短上下文定价
537
+ input_cost_per_mtok: ¥6.00
538
+ output_cost_per_mtok: ¥24.00
539
+ cache_read_cost_per_mtok: ¥1.30
544
540
  - vendor: zhipu
545
541
  model: glm-5.1 # 待区分长短上下文定价
546
542
  input_cost_per_mtok: ¥6.00
@@ -556,6 +552,21 @@ pricing:
556
552
  input_cost_per_mtok: ¥4.00
557
553
  output_cost_per_mtok: ¥18.00
558
554
  cache_read_cost_per_mtok: ¥1.00
555
+ - vendor: zhipu
556
+ model: glm-5v-turbo # 待区分长短上下文定价
557
+ input_cost_per_mtok: ¥5.00
558
+ output_cost_per_mtok: ¥22.00
559
+ cache_read_cost_per_mtok: ¥1.20
560
+ - vendor: zhipu
561
+ model: glm-4.7 # 待区分长短上下文定价
562
+ input_cost_per_mtok: ¥2.00
563
+ output_cost_per_mtok: ¥8.00
564
+ cache_read_cost_per_mtok: ¥0.40
565
+ - vendor: zhipu
566
+ model: glm-4.5-air # 待区分长短上下文定价
567
+ input_cost_per_mtok: ¥0.80
568
+ output_cost_per_mtok: ¥2.00
569
+ cache_read_cost_per_mtok: ¥0.16
559
570
  # ── MiniMax ──
560
571
  - vendor: minimax
561
572
  model: minimax-m2.7
@@ -685,4 +696,11 @@ native_api:
685
696
  # tiers: ["copilot", "anthropic", "zhipu"]
686
697
  #
687
698
  # 未配置时(默认),所有 Session 使用全局 tiers 顺序。
688
- session_policies: []
699
+ session_policies:
700
+ policies: []
701
+ # 标题前缀 → 供应商自动绑定。
702
+ # 当 Session 标题以指定前缀开头时,自动将该 Session 绑定到对应供应商。
703
+ # 匹配规则按列表顺序求值,首次匹配生效。
704
+ title_vendor_bindings:
705
+ - prefix: "# 目标"
706
+ vendor: "zhipu"
@@ -44,7 +44,7 @@ from .routing import ( # noqa: F401
44
44
 
45
45
  # ── 子模块 re-export ────────────────────────────────────────────
46
46
  from .server import DatabaseConfig, LoggingConfig, ServerConfig # noqa: F401
47
- from .session_policy import SessionPoliciesConfig # noqa: F401
47
+ from .session_policy import SessionPoliciesConfig, TitleVendorBinding # noqa: F401
48
48
  from .vendors import ( # noqa: F401
49
49
  AlibabaConfig,
50
50
  AnthropicConfig,
@@ -350,4 +350,5 @@ __all__ = [
350
350
  "NativeApiConfig",
351
351
  # session policy
352
352
  "SessionPoliciesConfig",
353
+ "TitleVendorBinding",
353
354
  ]
@@ -50,6 +50,22 @@ class SessionPolicy(BaseModel):
50
50
  )
51
51
 
52
52
 
53
+ class TitleVendorBinding(BaseModel):
54
+ """标题前缀 → 供应商自动绑定规则."""
55
+
56
+ prefix: str = Field(
57
+ min_length=1,
58
+ description=(
59
+ "标题前缀匹配模式(大小写敏感的 startswith 匹配)。"
60
+ "禁止空字符串——空前缀会匹配所有标题,导致全量误绑定。"
61
+ ),
62
+ )
63
+ vendor: str = Field(
64
+ min_length=1,
65
+ description="匹配后绑定的目标供应商名称",
66
+ )
67
+
68
+
53
69
  class SessionPoliciesConfig(BaseModel):
54
70
  """顶层 Session 策略配置容器."""
55
71
 
@@ -57,3 +73,11 @@ class SessionPoliciesConfig(BaseModel):
57
73
  default_factory=list,
58
74
  description="Session 路由策略列表,按定义顺序求值,首次匹配生效",
59
75
  )
76
+ title_vendor_bindings: list[TitleVendorBinding] = Field(
77
+ default_factory=list,
78
+ description=(
79
+ "标题前缀 → 供应商自动绑定规则。"
80
+ "当 Session 标题以指定前缀开头时,自动绑定到对应供应商。"
81
+ "匹配规则按列表顺序求值,首次匹配生效。"
82
+ ),
83
+ )
@@ -11,10 +11,13 @@ import logging
11
11
  import re
12
12
  import time
13
13
  from collections.abc import AsyncIterator
14
- from typing import Any
14
+ from typing import TYPE_CHECKING, Any
15
15
 
16
16
  import httpx
17
17
 
18
+ if TYPE_CHECKING:
19
+ from ..config.session_policy import TitleVendorBinding
20
+
18
21
  from ..vendors.base import (
19
22
  NoCompatibleVendorError,
20
23
  RequestCapabilities,
@@ -610,6 +613,7 @@ class _RouteExecutor:
610
613
  session_manager: RouteSessionManager,
611
614
  reauth_coordinator: Any | None = None,
612
615
  session_policy_resolver: SessionPolicyResolver | None = None,
616
+ title_vendor_bindings: list[TitleVendorBinding] | None = None,
613
617
  ) -> None:
614
618
  self._router = router
615
619
  self._tiers = tiers
@@ -617,6 +621,8 @@ class _RouteExecutor:
617
621
  self._session_mgr = session_manager
618
622
  self._reauth_coordinator = reauth_coordinator
619
623
  self._policy_resolver = session_policy_resolver or SessionPolicyResolver()
624
+ self._title_vendor_bindings = title_vendor_bindings or []
625
+ self._validate_title_vendor_bindings()
620
626
 
621
627
  # Tier 名称 → OAuth provider 名称的映射
622
628
  self._tier_provider_map: dict[str, str] = {
@@ -624,6 +630,26 @@ class _RouteExecutor:
624
630
  "antigravity": "google",
625
631
  }
626
632
 
633
+ def _validate_title_vendor_bindings(self) -> None:
634
+ """启动期校验标题绑定引用的 vendor 均存在,缺失则告警.
635
+
636
+ 与手动绑定 API(拒绝未知 vendor)的语义对齐:此处不硬失败,
637
+ 仅记录警告——避免单条误配置阻断整个代理启动;运行时
638
+ `_resolve_effective_tiers` 会静默跳过未知 vendor 回退默认顺序。
639
+ """
640
+ if not self._title_vendor_bindings:
641
+ return
642
+ valid = {t.name for t in self._tiers}
643
+ for binding in self._title_vendor_bindings:
644
+ if binding.vendor not in valid:
645
+ logger.warning(
646
+ "title_vendor_bindings 引用了未知 vendor %r(前缀 %r);"
647
+ "可用 vendor: %s。该绑定将在运行时被静默跳过。",
648
+ binding.vendor,
649
+ binding.prefix,
650
+ sorted(valid),
651
+ )
652
+
627
653
  # ── 公开执行入口 ──────────────────────────────────────
628
654
 
629
655
  def _resolve_effective_tiers(self, session_key: str) -> list[VendorTier]:
@@ -650,6 +676,27 @@ class _RouteExecutor:
650
676
  seen.add(tier.name)
651
677
  return ordered
652
678
 
679
+ def _apply_title_based_policy(self, session_key: str, title: str) -> None:
680
+ """根据 Session 标题前缀自动绑定供应商.
681
+
682
+ 当标题以预配置的前缀开头时,通过 SessionPolicyResolver.upsert()
683
+ 将该 Session 绑定到指定供应商,后续请求无需再走默认路由。
684
+
685
+ 仅在新 Session 首次提取标题时调用,避免覆盖手动绑定的策略。
686
+ """
687
+ if not title or not self._title_vendor_bindings:
688
+ return
689
+ for binding in self._title_vendor_bindings:
690
+ if title.startswith(binding.prefix):
691
+ self._policy_resolver.upsert(session_key, [binding.vendor])
692
+ logger.info(
693
+ "Session title prefix %r matched → auto-bind to %s (session=%s)",
694
+ binding.prefix,
695
+ binding.vendor,
696
+ session_key[:12],
697
+ )
698
+ return
699
+
653
700
  def _prepare_body_for_tier(
654
701
  self,
655
702
  body: dict[str, Any],
@@ -748,6 +795,7 @@ class _RouteExecutor:
748
795
  await self._recorder.set_session_title(
749
796
  canonical_request.session_key, title
750
797
  )
798
+ self._apply_title_based_policy(canonical_request.session_key, title)
751
799
  else:
752
800
  # 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
753
801
  title = _extract_session_title(canonical_request)
@@ -934,6 +982,7 @@ class _RouteExecutor:
934
982
  await self._recorder.set_session_title(
935
983
  canonical_request.session_key, title
936
984
  )
985
+ self._apply_title_based_policy(canonical_request.session_key, title)
937
986
  else:
938
987
  # 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
939
988
  title = _extract_session_title(canonical_request)
@@ -14,6 +14,7 @@ from collections.abc import AsyncIterator
14
14
  from typing import TYPE_CHECKING, Any
15
15
 
16
16
  if TYPE_CHECKING:
17
+ from ..config.session_policy import TitleVendorBinding
17
18
  from ..pricing import PricingTable
18
19
 
19
20
  from .executor import _RouteExecutor
@@ -38,6 +39,7 @@ class RequestRouter:
38
39
  reauth_coordinator: Any | None = None,
39
40
  compat_session_store: CompatSessionStore | None = None,
40
41
  session_policy_resolver: SessionPolicyResolver | None = None,
42
+ title_vendor_bindings: list[TitleVendorBinding] | None = None,
41
43
  ) -> None:
42
44
  if not tiers:
43
45
  raise ValueError("至少需要一个供应商层级")
@@ -56,6 +58,7 @@ class RequestRouter:
56
58
  session_manager=self._session_mgr,
57
59
  reauth_coordinator=reauth_coordinator,
58
60
  session_policy_resolver=session_policy_resolver,
61
+ title_vendor_bindings=title_vendor_bindings,
59
62
  )
60
63
 
61
64
  def set_pricing_table(self, table: PricingTable) -> None:
@@ -161,6 +161,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
161
161
  reauth_coordinator,
162
162
  compat_session_store,
163
163
  session_policy_resolver=SessionPolicyResolver(config.session_policies.policies),
164
+ title_vendor_bindings=config.session_policies.title_vendor_bindings,
164
165
  )
165
166
 
166
167
  app = FastAPI(title="coding-proxy", version=__version__, lifespan=lifespan)
@@ -1,4 +1,4 @@
1
- """智谱 GLM 供应商 — 原生 Anthropic 兼容端点代理(兼容转换 + 429 重试).
1
+ """智谱 GLM 供应商 — 原生 Anthropic 兼容端点代理(兼容转换 + 429/529 重试).
2
2
 
3
3
  官方端点 (https://open.bigmodel.cn/api/anthropic) 支持大部分
4
4
  Anthropic Messages API 协议,本模块做以下适配:
@@ -13,7 +13,8 @@ Anthropic Messages API 协议,本模块做以下适配:
13
13
  - reasoning_effort 参数:静默忽略
14
14
  - metadata 字段:暂不处理(待进一步诊断确认兼容性)
15
15
 
16
- 额外提供 429 Rate Limit 专用重试挽回机制:
16
+ 额外提供 429/529 专用重试挽回机制:
17
+ - 429 Rate Limit(限流)与 529 Overloaded(并发过载)共用同一退避策略
17
18
  - max_attempt = 5(1 初始 + 4 重试)
18
19
  - 指数退避 + Full Jitter(1s → 2s → 4s → 8s)
19
20
  - 优先尊重 server retry-after header
@@ -56,14 +57,20 @@ _RATE_LIMIT_RETRY = RetryConfig(
56
57
  jitter=True,
57
58
  )
58
59
 
60
+ # 触发指数退避重试的瞬态状态码(单一事实源)
61
+ # 429 Rate Limit(限流);529 Overloaded(并发过载)
62
+ # 二者均为服务端瞬态过载信号,适用同一退避挽回策略。
63
+ _BACKOFF_RETRY_STATUS = frozenset({429, 529})
64
+
59
65
 
60
66
  class ZhipuVendor(NativeAnthropicVendor):
61
- """智谱 GLM 原生 Anthropic 兼容端点供应商(薄透传 + 429 重试挽回).
67
+ """智谱 GLM 原生 Anthropic 兼容端点供应商(薄透传 + 429/529 重试挽回).
62
68
 
63
69
  通过官方 /api/anthropic 端点转发请求,
64
70
  仅替换模型名和认证头,其余原样透传。
65
71
 
66
- 429 Rate Limit 时自动重试(指数退避),降低 failover 频率。
72
+ 429 Rate Limit(限流)/ 529 Overloaded(并发过载)时自动重试
73
+ (指数退避),降低 failover 频率。
67
74
  并发限流由 BaseVendor._concurrency_controller 统一管控。
68
75
  """
69
76
 
@@ -124,24 +131,25 @@ class ZhipuVendor(NativeAnthropicVendor):
124
131
 
125
132
  return body, new_headers
126
133
 
127
- # ── 非流式:429 重试 ────────────────────────────────────
134
+ # ── 非流式:429/529 重试 ────────────────────────────────
128
135
 
129
136
  async def send_message(
130
137
  self,
131
138
  request_body: dict[str, Any],
132
139
  headers: dict[str, str],
133
140
  ) -> VendorResponse:
134
- """非流式请求,429 时自动重试."""
141
+ """非流式请求,429/529 时自动重试."""
135
142
  max_attempts = self._rl_retry.max_attempts
136
143
 
137
144
  for attempt in range(max_attempts):
138
145
  resp = await super().send_message(request_body, headers)
139
- if resp.status_code != 429:
146
+ if resp.status_code not in _BACKOFF_RETRY_STATUS:
140
147
  return resp
141
148
 
142
149
  if attempt == max_attempts - 1:
143
150
  logger.warning(
144
- "Zhipu 429 rate limit exhausted after %d attempts",
151
+ "Zhipu %d overload exhausted after %d attempts",
152
+ resp.status_code,
145
153
  max_attempts,
146
154
  )
147
155
  return resp
@@ -150,7 +158,8 @@ class ZhipuVendor(NativeAnthropicVendor):
150
158
  resp.response_headers, attempt
151
159
  )
152
160
  logger.info(
153
- "Zhipu 429 rate limit, retry %d/%d in %.1fms",
161
+ "Zhipu %d overload, retry %d/%d in %.1fms",
162
+ resp.status_code,
154
163
  attempt + 1,
155
164
  max_attempts - 1,
156
165
  delay,
@@ -159,16 +168,16 @@ class ZhipuVendor(NativeAnthropicVendor):
159
168
 
160
169
  return resp # pragma: no cover
161
170
 
162
- # ── 流式:429 重试 ──────────────────────────────────────
171
+ # ── 流式:429/529 重试 ──────────────────────────────────
163
172
 
164
173
  async def send_message_stream(
165
174
  self,
166
175
  request_body: dict[str, Any],
167
176
  headers: dict[str, str],
168
177
  ) -> AsyncIterator[bytes]:
169
- """流式请求,429 时自动重试.
178
+ """流式请求,429/529 时自动重试.
170
179
 
171
- 安全性:429 在 BaseVendor.send_message_stream 中于
180
+ 安全性:429/529 在 BaseVendor.send_message_stream 中于
172
181
  status code 检查阶段即 raise(在任何 chunk yield 之前),
173
182
  因此重试不会导致已发出数据不一致。
174
183
  """
@@ -176,25 +185,33 @@ class ZhipuVendor(NativeAnthropicVendor):
176
185
 
177
186
  for attempt in range(max_attempts):
178
187
  try:
179
- # 429 在 status code 检查阶段即 raise(在任何 chunk 之前),
188
+ # 429/529 在 status code 检查阶段即 raise(在任何 chunk 之前),
180
189
  # 因此 __anext__ 安全:要么拿到首个 chunk,要么抛异常。
181
190
  ait = super().send_message_stream(request_body, headers)
182
191
  head = await ait.__anext__()
183
192
  except StopAsyncIteration:
184
193
  return
185
194
  except httpx.HTTPStatusError as exc:
186
- if exc.response is None or exc.response.status_code != 429:
195
+ if (
196
+ exc.response is None
197
+ or exc.response.status_code not in _BACKOFF_RETRY_STATUS
198
+ ):
187
199
  raise
200
+ status_code = exc.response.status_code
188
201
  if attempt == max_attempts - 1:
189
202
  logger.warning(
190
- "Zhipu 429 stream rate limit exhausted after %d attempts",
203
+ "Zhipu %d stream overload exhausted after %d attempts",
204
+ status_code,
191
205
  max_attempts,
192
206
  )
193
207
  raise
194
208
 
195
- delay = self._compute_retry_delay_from_response(exc.response, attempt)
209
+ delay = self._compute_retry_delay_from_headers(
210
+ exc.response.headers, attempt
211
+ )
196
212
  logger.info(
197
- "Zhipu 429 stream rate limit, retry %d/%d in %.1fms",
213
+ "Zhipu %d stream overload, retry %d/%d in %.1fms",
214
+ status_code,
198
215
  attempt + 1,
199
216
  max_attempts - 1,
200
217
  delay,
@@ -215,24 +232,15 @@ class ZhipuVendor(NativeAnthropicVendor):
215
232
  headers: dict[str, str] | None,
216
233
  attempt: int,
217
234
  ) -> float:
218
- """计算重试延迟(毫秒),优先使用 server retry-after."""
219
- rl_info = parse_rate_limit_headers(headers, 429, None)
220
- server_delay_s = compute_effective_retry_seconds(rl_info)
221
- if server_delay_s is not None:
222
- return min(server_delay_s * 1000, self._rl_retry.max_delay_ms)
223
- return calculate_delay(attempt, self._rl_retry)
235
+ """计算重试延迟(毫秒),优先使用 server retry-after.
224
236
 
225
- def _compute_retry_delay_from_response(
226
- self,
227
- response: httpx.Response,
228
- attempt: int,
229
- ) -> float:
230
- """计算重试延迟(毫秒),从 httpx.Response 提取 header."""
231
- rl_info = parse_rate_limit_headers(
232
- response.headers,
233
- response.status_code,
234
- response.text[:500] if response.text else None,
235
- )
237
+ 非流式与流式 429/529 重试共用此方法(单一延迟逻辑)。
238
+ 以"限流退避"语义(429)解析 header:``parse_rate_limit_headers``
239
+ 仅对 429/403 解析 retry-after,故此处固定传 429,
240
+ 使 529 也能尊重 server retry-after,与 429 行为一致。
241
+ server 信号时回退到指数退避 + Full Jitter。
242
+ """
243
+ rl_info = parse_rate_limit_headers(headers, 429, None)
236
244
  server_delay_s = compute_effective_retry_seconds(rl_info)
237
245
  if server_delay_s is not None:
238
246
  return min(server_delay_s * 1000, self._rl_retry.max_delay_ms)