coding-proxy 0.5.1a7__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.1a7 → coding_proxy-0.5.1a9}/PKG-INFO +1 -1
  2. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/design-patterns.md +2 -0
  3. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/pyproject.toml +1 -1
  4. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/config.default.yaml +45 -34
  5. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/zhipu.py +42 -34
  6. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_zhipu.py +181 -0
  7. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/uv.lock +1 -1
  8. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.github/workflows/ci.yml +0 -0
  9. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.github/workflows/coverage.yml +0 -0
  10. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.github/workflows/release.yml +0 -0
  11. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.gitignore +0 -0
  12. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/.pre-commit-config.yaml +0 -0
  13. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/AGENTS.md +0 -0
  14. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/CHANGELOG.md +0 -0
  15. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/CLAUDE.md +0 -0
  16. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/LICENSE +0 -0
  17. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/README.md +0 -0
  18. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/assets/dashboard-v0.4.0.png +0 -0
  19. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/assets/model-calling-v0.5.0.png +0 -0
  20. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/assets/session-v0.4.0.png +0 -0
  21. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/browser-validation.md +0 -0
  22. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/issue.md +0 -0
  23. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/knowledge-map.md +0 -0
  24. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/.agents/reference-specifications.md +0 -0
  25. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/config-reference.md +0 -0
  26. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/convert.md +0 -0
  27. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/routing.md +0 -0
  28. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/testing.md +0 -0
  29. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/arch/vendors.md +0 -0
  30. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/framework.md +0 -0
  31. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/api-reference.md +0 -0
  32. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/cli-reference.md +0 -0
  33. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/dashboard.md +0 -0
  34. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/monitoring.md +0 -0
  35. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/quickstart.md +0 -0
  36. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/guide/vendors.md +0 -0
  37. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/ops/ci-cd.md +0 -0
  38. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/user-guide.md +0 -0
  39. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/docs/zh-CN/README.md +0 -0
  40. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/__init__.py +0 -0
  41. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/__init__.py +0 -0
  42. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/__main__.py +0 -0
  43. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/__init__.py +0 -0
  44. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/__init__.py +0 -0
  45. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/base.py +0 -0
  46. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/github.py +0 -0
  47. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/providers/google.py +0 -0
  48. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/runtime.py +0 -0
  49. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/auth/store.py +0 -0
  50. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/__init__.py +0 -0
  51. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/auth_commands.py +0 -0
  52. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/cli/banner.py +0 -0
  53. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/__init__.py +0 -0
  54. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/canonical.py +0 -0
  55. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/compat/session_store.py +0 -0
  56. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/__init__.py +0 -0
  57. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/auth_schema.py +0 -0
  58. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/loader.py +0 -0
  59. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/resiliency.py +0 -0
  60. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/routing.py +0 -0
  61. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/schema.py +0 -0
  62. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/server.py +0 -0
  63. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/session_policy.py +0 -0
  64. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/config/vendors.py +0 -0
  65. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/__init__.py +0 -0
  66. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  67. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  68. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  69. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  70. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  71. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/convert/vendor_channels.py +0 -0
  72. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/__init__.py +0 -0
  73. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/db.py +0 -0
  74. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/formatters.py +0 -0
  75. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/logging/stats.py +0 -0
  76. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/__init__.py +0 -0
  77. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/auth.py +0 -0
  78. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/compat.py +0 -0
  79. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/constants.py +0 -0
  80. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/pricing.py +0 -0
  81. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/token.py +0 -0
  82. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/model/vendor.py +0 -0
  83. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/__init__.py +0 -0
  84. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/config.py +0 -0
  85. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  86. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  87. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  88. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  89. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/handler.py +0 -0
  90. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/operation.py +0 -0
  91. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/routes.py +0 -0
  92. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/native_api/usage_registry.py +0 -0
  93. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/pricing.py +0 -0
  94. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/__init__.py +0 -0
  95. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  96. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/error_classifier.py +0 -0
  97. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/executor.py +0 -0
  98. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/model_mapper.py +0 -0
  99. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/quota_guard.py +0 -0
  100. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/rate_limit.py +0 -0
  101. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/retry.py +0 -0
  102. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/router.py +0 -0
  103. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/session_manager.py +0 -0
  104. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/session_policy.py +0 -0
  105. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/tier.py +0 -0
  106. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/usage_parser.py +0 -0
  107. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/routing/usage_recorder.py +0 -0
  108. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/__init__.py +0 -0
  109. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/app.py +0 -0
  110. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/dashboard.py +0 -0
  111. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/factory.py +0 -0
  112. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/responses.py +0 -0
  113. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/server/routes.py +0 -0
  114. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/streaming/__init__.py +0 -0
  115. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  116. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/__init__.py +0 -0
  117. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/alibaba.py +0 -0
  118. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/anthropic.py +0 -0
  119. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/antigravity.py +0 -0
  120. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/base.py +0 -0
  121. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/concurrency.py +0 -0
  122. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot.py +0 -0
  123. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_models.py +0 -0
  124. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  125. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  126. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/doubao.py +0 -0
  127. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/kimi.py +0 -0
  128. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/minimax.py +0 -0
  129. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/mixins.py +0 -0
  130. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  131. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/token_manager.py +0 -0
  132. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/src/coding/proxy/vendors/xiaomi.py +0 -0
  133. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/__init__.py +0 -0
  134. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/__init__.py +0 -0
  135. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/conftest.py +0 -0
  136. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_http.py +0 -0
  137. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_token.py +0 -0
  138. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/e2e/test_e2e_vendor.py +0 -0
  139. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_antigravity.py +0 -0
  140. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_app_routes.py +0 -0
  141. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_auto_login.py +0 -0
  142. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_banner.py +0 -0
  143. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_circuit_breaker.py +0 -0
  144. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_cli_usage.py +0 -0
  145. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_compat.py +0 -0
  146. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_concurrency_monitor.py +0 -0
  147. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_config_init.py +0 -0
  148. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_config_loader.py +0 -0
  149. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_convert_request.py +0 -0
  150. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_convert_response.py +0 -0
  151. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_convert_sse.py +0 -0
  152. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot.py +0 -0
  153. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_convert_request.py +0 -0
  154. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_convert_response.py +0 -0
  155. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_models.py +0 -0
  156. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_copilot_urls.py +0 -0
  157. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_currency.py +0 -0
  158. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_error_classifier.py +0 -0
  159. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_executor_in_flight_tracking.py +0 -0
  160. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_logging_dual_write.py +0 -0
  161. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_mixins.py +0 -0
  162. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_auth.py +0 -0
  163. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_compat.py +0 -0
  164. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_constants.py +0 -0
  165. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_mapper.py +0 -0
  166. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_pricing.py +0 -0
  167. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_token.py +0 -0
  168. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_model_vendor.py +0 -0
  169. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_base_url_override.py +0 -0
  170. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_extractors.py +0 -0
  171. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_handler.py +0 -0
  172. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_operation.py +0 -0
  173. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_api_routes.py +0 -0
  174. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_native_vendors.py +0 -0
  175. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_parse_usage.py +0 -0
  176. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_parse_usage_gemini.py +0 -0
  177. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_pricing.py +0 -0
  178. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_quota_guard.py +0 -0
  179. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_rate_limit.py +0 -0
  180. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_router_chain.py +0 -0
  181. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_router_executor.py +0 -0
  182. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_runtime_reauth.py +0 -0
  183. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_schema.py +0 -0
  184. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_session_aware.py +0 -0
  185. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_streaming_anthropic_compat.py +0 -0
  186. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_tier.py +0 -0
  187. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_tiers_config.py +0 -0
  188. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_time_range.py +0 -0
  189. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_token_logger.py +0 -0
  190. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_token_logger_native_columns.py +0 -0
  191. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_token_manager.py +0 -0
  192. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_types.py +0 -0
  193. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_vendor_channels.py +0 -0
  194. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_vendor_streaming.py +0 -0
  195. {coding_proxy-0.5.1a7 → coding_proxy-0.5.1a9}/tests/test_vendors.py +0 -0
  196. {coding_proxy-0.5.1a7 → 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.1a7
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.1a7"
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
@@ -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)
@@ -356,6 +356,20 @@ def _make_429_response(
356
356
  )
357
357
 
358
358
 
359
+ def _make_529_response(
360
+ headers: dict[str, str] | None = None,
361
+ ) -> httpx.Response:
362
+ """构造 529 HTTP 响应(Overloaded / 并发过载)."""
363
+ return httpx.Response(
364
+ status_code=529,
365
+ content=b'{"error":{"type":"overloaded_error","message":"Overloaded"}}',
366
+ headers=headers or {},
367
+ request=httpx.Request(
368
+ "POST", "https://open.bigmodel.cn/api/anthropic/v1/messages"
369
+ ),
370
+ )
371
+
372
+
359
373
  def _make_200_response() -> httpx.Response:
360
374
  """构造 200 HTTP 响应."""
361
375
  body = json.dumps(
@@ -666,3 +680,170 @@ class TestRateLimitRetry:
666
680
  {},
667
681
  )
668
682
  assert resp.status_code == 401
683
+
684
+ # ── 529 Overloaded(并发过载)重试,行为与 429 一致 ──────
685
+
686
+ @pytest.mark.asyncio
687
+ async def test_nonstream_529_retries_and_succeeds(self):
688
+ """非流式 529 两次后 200,重试成功."""
689
+ vendor = _make_zhipu_vendor()
690
+ call_count = 0
691
+
692
+ async def mock_post(*args, **kwargs):
693
+ nonlocal call_count
694
+ call_count += 1
695
+ if call_count <= 2:
696
+ return _make_529_response()
697
+ return _make_200_response()
698
+
699
+ with (
700
+ patch.object(vendor, "_get_client") as mock_client,
701
+ patch("asyncio.sleep", new_callable=AsyncMock),
702
+ ):
703
+ client = AsyncMock()
704
+ client.post = mock_post
705
+ mock_client.return_value = client
706
+
707
+ resp = await vendor.send_message(
708
+ {"model": "claude-sonnet-4-20250514", "messages": []},
709
+ {},
710
+ )
711
+
712
+ assert resp.status_code == 200
713
+ assert call_count == 3
714
+
715
+ @pytest.mark.asyncio
716
+ async def test_nonstream_529_exhausted_retries(self):
717
+ """非流式连续 5 次 529,耗尽重试后返回 529."""
718
+ vendor = _make_zhipu_vendor()
719
+ call_count = 0
720
+
721
+ async def mock_post(*args, **kwargs):
722
+ nonlocal call_count
723
+ call_count += 1
724
+ return _make_529_response()
725
+
726
+ with (
727
+ patch.object(vendor, "_get_client") as mock_client,
728
+ patch("asyncio.sleep", new_callable=AsyncMock),
729
+ ):
730
+ client = AsyncMock()
731
+ client.post = mock_post
732
+ mock_client.return_value = client
733
+
734
+ resp = await vendor.send_message(
735
+ {"model": "claude-sonnet-4-20250514", "messages": []},
736
+ {},
737
+ )
738
+
739
+ assert resp.status_code == 529
740
+ assert call_count == 5
741
+
742
+ @pytest.mark.asyncio
743
+ async def test_stream_529_retries_and_succeeds(self):
744
+ """流式 529 两次后成功."""
745
+ call_count = 0
746
+
747
+ async def fake_stream(self, body, headers):
748
+ nonlocal call_count
749
+ call_count += 1
750
+ if call_count <= 2:
751
+ resp = _make_529_response()
752
+ raise httpx.HTTPStatusError(
753
+ "529",
754
+ request=resp.request,
755
+ response=resp,
756
+ )
757
+ yield b'data: {"type":"content_block_start"}\n\n'
758
+ yield b'data: {"type":"content_block_delta"}\n\n'
759
+
760
+ vendor = _make_zhipu_vendor()
761
+ chunks = []
762
+ with (
763
+ patch.object(NativeAnthropicVendor, "send_message_stream", fake_stream),
764
+ patch("asyncio.sleep", new_callable=AsyncMock),
765
+ ):
766
+ async for chunk in vendor.send_message_stream(
767
+ {"model": "claude-sonnet-4-20250514", "messages": []},
768
+ {},
769
+ ):
770
+ chunks.append(chunk)
771
+
772
+ assert len(chunks) == 2
773
+ assert call_count == 3
774
+
775
+ @pytest.mark.asyncio
776
+ async def test_stream_529_exhausted_retries_raises(self):
777
+ """流式连续 529,耗尽重试后 raise."""
778
+ call_count = 0
779
+
780
+ async def fake_stream(self, body, headers):
781
+ nonlocal call_count
782
+ call_count += 1
783
+ resp = _make_529_response()
784
+ raise httpx.HTTPStatusError(
785
+ "529",
786
+ request=resp.request,
787
+ response=resp,
788
+ )
789
+ yield # 使函数成为 async generator(不可达,仅影响类型)
790
+
791
+ vendor = _make_zhipu_vendor()
792
+ with (
793
+ patch.object(NativeAnthropicVendor, "send_message_stream", fake_stream),
794
+ patch("asyncio.sleep", new_callable=AsyncMock),
795
+ pytest.raises(httpx.HTTPStatusError) as exc_info,
796
+ ):
797
+ async for _ in vendor.send_message_stream(
798
+ {"model": "claude-sonnet-4-20250514", "messages": []},
799
+ {},
800
+ ):
801
+ pass
802
+
803
+ assert exc_info.value.response.status_code == 529
804
+ assert call_count == 5
805
+
806
+ @pytest.mark.asyncio
807
+ async def test_stream_529_respects_retry_after(self):
808
+ """流式 529 响应含 retry-after 时使用 server 建议延迟.
809
+
810
+ 回归保障:修复前流式延迟计算把真实状态码 529 传给
811
+ parse_rate_limit_headers(仅对 429/403 解析),导致 529 忽略
812
+ retry-after 而回退指数退避(首次最多 1s)。修复后固定按 429
813
+ 语义解析,529 与 429 一样尊重 server retry-after。
814
+ """
815
+ call_count = 0
816
+ sleep_delays = []
817
+
818
+ async def fake_stream(self, body, headers):
819
+ nonlocal call_count
820
+ call_count += 1
821
+ if call_count == 1:
822
+ resp = _make_529_response(headers={"retry-after": "2"})
823
+ raise httpx.HTTPStatusError(
824
+ "529",
825
+ request=resp.request,
826
+ response=resp,
827
+ )
828
+ yield b'data: {"type":"content_block_start"}\n\n'
829
+
830
+ async def mock_sleep(delay):
831
+ sleep_delays.append(delay)
832
+
833
+ vendor = _make_zhipu_vendor()
834
+ chunks = []
835
+ with (
836
+ patch.object(NativeAnthropicVendor, "send_message_stream", fake_stream),
837
+ patch("asyncio.sleep", side_effect=mock_sleep),
838
+ ):
839
+ async for chunk in vendor.send_message_stream(
840
+ {"model": "claude-sonnet-4-20250514", "messages": []},
841
+ {},
842
+ ):
843
+ chunks.append(chunk)
844
+
845
+ assert len(chunks) == 1
846
+ assert call_count == 2
847
+ assert len(sleep_delays) == 1
848
+ # retry-after=2 → 2 * 1.1 = 2.2s(>1s 指数退避首跳,证明用了 server 信号)
849
+ assert 2.0 <= sleep_delays[0] <= 2.2
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "coding-proxy"
77
- version = "0.5.1a7"
77
+ version = "0.5.1a9"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes