coding-proxy 0.3.1a5__tar.gz → 0.3.1a7__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 (179) hide show
  1. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/AGENTS.md +1 -0
  2. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/PKG-INFO +1 -1
  3. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/issue.md +47 -13
  4. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/pyproject.toml +1 -1
  5. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/vendor_channels.py +122 -57
  6. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/vendor.py +2 -0
  7. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/error_classifier.py +13 -5
  8. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_error_classifier.py +72 -0
  9. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_vendor.py +12 -3
  10. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_router_executor.py +5 -10
  11. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_types.py +1 -0
  12. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_vendor_channels.py +83 -110
  13. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_vendors.py +36 -2
  14. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/uv.lock +1 -1
  15. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.github/workflows/ci.yml +0 -0
  16. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.github/workflows/coverage.yml +0 -0
  17. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.github/workflows/release.yml +0 -0
  18. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.gitignore +0 -0
  19. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/.pre-commit-config.yaml +0 -0
  20. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/CHANGELOG.md +0 -0
  21. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/CLAUDE.md +0 -0
  22. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/LICENSE +0 -0
  23. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/README.md +0 -0
  24. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/assets/dashboard-v0.2.4.png +0 -0
  25. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/config-reference.md +0 -0
  26. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/convert.md +0 -0
  27. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/design-patterns.md +0 -0
  28. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/routing.md +0 -0
  29. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/testing.md +0 -0
  30. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/arch/vendors.md +0 -0
  31. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/ci-cd.md +0 -0
  32. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/framework.md +0 -0
  33. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/api-reference.md +0 -0
  34. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/cli-reference.md +0 -0
  35. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/dashboard.md +0 -0
  36. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/monitoring.md +0 -0
  37. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/quickstart.md +0 -0
  38. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/guide/vendors.md +0 -0
  39. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/user-guide.md +0 -0
  40. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/docs/zh-CN/README.md +0 -0
  41. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/__init__.py +0 -0
  42. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/__init__.py +0 -0
  43. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/__main__.py +0 -0
  44. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/__init__.py +0 -0
  45. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/__init__.py +0 -0
  46. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/base.py +0 -0
  47. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/github.py +0 -0
  48. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/google.py +0 -0
  49. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/runtime.py +0 -0
  50. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/store.py +0 -0
  51. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/__init__.py +0 -0
  52. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/auth_commands.py +0 -0
  53. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/banner.py +0 -0
  54. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/__init__.py +0 -0
  55. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/canonical.py +0 -0
  56. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/session_store.py +0 -0
  57. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/__init__.py +0 -0
  58. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/auth_schema.py +0 -0
  59. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/config.default.yaml +0 -0
  60. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/loader.py +0 -0
  61. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/resiliency.py +0 -0
  62. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/routing.py +0 -0
  63. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/schema.py +0 -0
  64. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/server.py +0 -0
  65. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/config/vendors.py +0 -0
  66. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/__init__.py +0 -0
  67. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  68. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  69. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  70. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  71. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  72. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/__init__.py +0 -0
  73. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/db.py +0 -0
  74. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/formatters.py +0 -0
  75. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/stats.py +0 -0
  76. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/__init__.py +0 -0
  77. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/auth.py +0 -0
  78. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/compat.py +0 -0
  79. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/constants.py +0 -0
  80. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/pricing.py +0 -0
  81. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/model/token.py +0 -0
  82. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/__init__.py +0 -0
  83. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/config.py +0 -0
  84. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  85. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  86. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  87. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  88. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/handler.py +0 -0
  89. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/operation.py +0 -0
  90. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/routes.py +0 -0
  91. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/usage_registry.py +0 -0
  92. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/pricing.py +0 -0
  93. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/__init__.py +0 -0
  94. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  95. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/executor.py +0 -0
  96. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/model_mapper.py +0 -0
  97. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/quota_guard.py +0 -0
  98. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/rate_limit.py +0 -0
  99. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/retry.py +0 -0
  100. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/router.py +0 -0
  101. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/session_manager.py +0 -0
  102. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/tier.py +0 -0
  103. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_parser.py +0 -0
  104. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_recorder.py +0 -0
  105. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/__init__.py +0 -0
  106. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/app.py +0 -0
  107. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/dashboard.py +0 -0
  108. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/factory.py +0 -0
  109. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/responses.py +0 -0
  110. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/server/routes.py +0 -0
  111. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/__init__.py +0 -0
  112. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  113. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/__init__.py +0 -0
  114. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/alibaba.py +0 -0
  115. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/anthropic.py +0 -0
  116. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/antigravity.py +0 -0
  117. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/base.py +0 -0
  118. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot.py +0 -0
  119. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_models.py +0 -0
  120. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  121. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  122. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/doubao.py +0 -0
  123. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/kimi.py +0 -0
  124. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/minimax.py +0 -0
  125. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/mixins.py +0 -0
  126. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  127. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/token_manager.py +0 -0
  128. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/xiaomi.py +0 -0
  129. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/zhipu.py +0 -0
  130. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/__init__.py +0 -0
  131. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_antigravity.py +0 -0
  132. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_app_routes.py +0 -0
  133. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_auto_login.py +0 -0
  134. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_banner.py +0 -0
  135. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_circuit_breaker.py +0 -0
  136. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_cli_usage.py +0 -0
  137. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_compat.py +0 -0
  138. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_config_init.py +0 -0
  139. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_config_loader.py +0 -0
  140. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_convert_request.py +0 -0
  141. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_convert_response.py +0 -0
  142. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_convert_sse.py +0 -0
  143. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot.py +0 -0
  144. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_request.py +0 -0
  145. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_response.py +0 -0
  146. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_models.py +0 -0
  147. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_copilot_urls.py +0 -0
  148. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_currency.py +0 -0
  149. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_logging_dual_write.py +0 -0
  150. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_mixins.py +0 -0
  151. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_auth.py +0 -0
  152. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_compat.py +0 -0
  153. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_constants.py +0 -0
  154. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_mapper.py +0 -0
  155. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_pricing.py +0 -0
  156. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_model_token.py +0 -0
  157. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_base_url_override.py +0 -0
  158. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_extractors.py +0 -0
  159. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_handler.py +0 -0
  160. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_operation.py +0 -0
  161. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_api_routes.py +0 -0
  162. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_native_vendors.py +0 -0
  163. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_parse_usage.py +0 -0
  164. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_parse_usage_gemini.py +0 -0
  165. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_pricing.py +0 -0
  166. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_quota_guard.py +0 -0
  167. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_rate_limit.py +0 -0
  168. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_router_chain.py +0 -0
  169. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_runtime_reauth.py +0 -0
  170. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_schema.py +0 -0
  171. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_streaming_anthropic_compat.py +0 -0
  172. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_tier.py +0 -0
  173. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_tiers_config.py +0 -0
  174. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_time_range.py +0 -0
  175. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_token_logger.py +0 -0
  176. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_token_logger_native_columns.py +0 -0
  177. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_token_manager.py +0 -0
  178. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_vendor_streaming.py +0 -0
  179. {coding_proxy-0.3.1a5 → coding_proxy-0.3.1a7}/tests/test_zhipu.py +0 -0
@@ -52,6 +52,7 @@
52
52
  1. **Python**: 严禁使用 pip/poetry,**必须**统一使用 `uv` 进行包管理与脚本执行(如 `uv run`);
53
53
  2. **JavaScript/TypeScript**: 严禁使用 npm/yarn,**必须**统一使用 `pnpm` 进行包管理与脚本执行。
54
54
  - **Database Management**: 谨慎操作,数据迁移、测试等操作严禁将现有数据删除,谨慎操作数据迁移的回滚,防止数据被清理。
55
+ - **In-depth and close to the facts**:系统且全面地进行问题的分析,深入贴近事实,如有疑问,需先发问,不要乱做决定。
55
56
 
56
57
  ## Documentation Standards (文档规范)
57
58
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.3.1a5
3
+ Version: 0.3.1a7
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
@@ -152,30 +152,64 @@ zhipu GLM-5 在处理含 `tool_result` 块的会话时持续返回 500 错误,
152
152
 
153
153
  ```
154
154
  WARNING zhipu stream error: status=500 body='...message":"\'ClaudeContentBlockToolResult\' object has no attribute \'id\'"}'
155
- WARNING Tier zhipu zhipu tool_result format error (500), treating as format incompatibility without circuit breaker penalty
156
- INFO Failover: zhipu → copilot (reason: HTTP 500)
157
155
  ```
158
156
 
159
157
  **表因**
160
158
 
161
- zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id` 字段(`id` 是 `tool_use` 块的属性)。
159
+ zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id` 字段。
162
160
 
163
- **根因**
161
+ **根因**(2026-04-29 第二次复盘更新)
164
162
 
165
- 所有 targeting zhipu 的转换通道(`prepare_zhipu_self_cleanup`、`prepare_copilot_to_zhipu`、`prepare_anthropic_to_zhipu`)在完成 `enforce_anthropic_tool_pairing` 后,没有为 `tool_result` 块补上 zhipu 后端期望的 `id` 字段。搬迁或合成的 `tool_result` 块仅有 `tool_use_id`,缺少 `id`。
163
+ **第一次诊断**(已推翻):认为 `_inject_tool_result_id_for_zhipu` 注入 `id` 可绕过。实证:注入 114 个块后 500 依旧。
166
164
 
167
- **处理方式**
165
+ **第二次诊断**(已推翻):认为 `enforce_anthropic_tool_pairing` 搬迁 tool_result 到 user 消息是触发条件。实证:移除 tool pairing 后 500 依旧(日志显示 `copilot → zhipu: stripped_19_thinking_blocks, removed_thinking_param`,无 `misplaced_tool_result_relocated`)。
166
+
167
+ **实际根因**:zhipu 后端的 `ClaudeContentBlockToolResult` Python 类**没有 `id` 属性**,但 zhipu 代码在处理**所有** `tool_result` 块时都访问 `obj.id`,无论块位于 assistant 还是 user 消息。三层因果链:
168
+
169
+ 1. **zhipu 后端 Bug**(不可修复 — 上游代码):`ClaudeContentBlockToolResult` 类缺少 `id` 属性,zhipu 代码访问时触发 `AttributeError` → 500。
170
+ 2. **JSON 注入无效**(已实证):`_inject_tool_result_id_for_zhipu` 往 JSON dict 注入 `id = tool_use_id`,但 zhipu 反序列化框架不读取此字段,Python 对象仍无 `id` 属性。
171
+ 3. **无预防机制**(proxy 层可修复):tier 门控系统不检查请求是否含 `tool_result` 块 → 每次请求先发 zhipu → 必然 500 → failover → 额外 ~2 秒延迟。
172
+
173
+ **实证依据**:
174
+ - 有注入(114 个块)→ 500;无注入 → 500。结论:注入无效。
175
+ - 有 tool pairing → 500;无 tool pairing → 500。结论:tool pairing 不是触发条件。
176
+ - 首次请求(无 tool_result 块)→ zhipu 正常。结论:500 由 tool_result 块本身触发。
177
+
178
+ **处理方式**(2026-04-29 第二次更新)
179
+
180
+ 从所有 zhipu 目标转换通道中移除有害/无效步骤(`enforce_anthropic_tool_pairing`、`_inject_tool_result_id_for_zhipu`、`_strip_cache_control`),并在 `ZhipuVendor.supports_request` 中增加 `has_tool_results` 门控:当请求包含 `tool_result` 块时主动拒绝 zhipu tier,避免「尝试 → 500 → failover」的无效延迟。
181
+
182
+ | 变更项 | 说明 |
183
+ |--------|------|
184
+ | `RequestCapabilities.has_tool_results` | 新增字段,检测请求中是否含 `tool_result` 块 |
185
+ | `CapabilityLossReason.TOOL_RESULTS` | 新增枚举值,标记 tool_result 兼容性问题 |
186
+ | `ZhipuVendor.supports_request` | 覆写方法,`has_tool_results=True` 时拒绝请求 |
187
+ | `build_request_capabilities` | 扩展 tool_result 块检测逻辑 |
188
+
189
+ 保留的 zhipu 目标转换通道精简步骤:
190
+
191
+ | 保留项 | 原因 |
192
+ |--------|------|
193
+ | `strip_thinking_blocks` | copilot/anthropic 的 thinking 签名 zhipu 无法验证 |
194
+ | 移除 `thinking`/`extended_thinking` 顶层参数 | zhipu 不支持 |
195
+ | `_remove_vendor_blocks(server_tool_use_delta)` | zhipu 自身流式残块 |
196
+ | `_remove_vendor_blocks(server_tool_use)` | Anthropic beta 块,zhipu 不支持 |
197
+
198
+ **涉及变更的转换通道**:
199
+ - `prepare_copilot_to_zhipu` — 移除 cache_control / tool pairing / id 注入
200
+ - `prepare_anthropic_to_zhipu` — 移除 cache_control / tool pairing / id 注入
201
+ - `prepare_zhipu_self_cleanup` — 移除 tool pairing / id 注入
168
202
 
169
- - `vendor_channels.py` 新增 `_inject_tool_result_id_for_zhipu` 辅助函数:扫描所有消息中的 `tool_result` 块,将 `tool_use_id` 值复制为 `id` 字段(仅注入尚无 `id` 的块,保持幂等)
170
- - 在三个 targeting zhipu 的转换通道末尾统一调用此辅助函数
171
- - 保留 executor 中已有的 500 错误检测作为纵深防御
203
+ **注意**: `prepare_zhipu_to_anthropic` `prepare_zhipu_to_copilot` 不受影响(目标是 anthropic/copilot,不是 zhipu),仍保留 `enforce_anthropic_tool_pairing`。
172
204
 
173
205
  **后续防范**
174
206
 
175
- - 其他 `NativeAnthropicVendor` 子类若出现类似的「后端期望非标准字段」问题,可参考此模式在对应的转换通道中注入兼容字段。
176
- - zhipu 后端修复此 bug(不再访问 `.id`)后,此 workaround 仍安全保留(多一个 `id` 字段不影响 Anthropic API 语义)。
207
+ - **转换通道的「最小干预」原则**:跨供应商转换应仅清理目标供应商**确认不支持**的特性。未经验证的「预防性清理」(如剥离 cache_control)可能误伤供应商原生支持的功能,甚至引入新的故障。
208
+ - **workaround 须验证有效**:`_inject_tool_result_id_for_zhipu` 虽有注释说明目的,但未经验证其有效性即合入。后续 workaround 须附带验证证据(如 curl 复现、上游确认)。
209
+ - **zhipu 后端 bug 跟踪**:`ClaudeContentBlockToolResult` 类缺少 `id` 属性是 zhipu 上游 bug。若 zhipu 修复此 bug,可考虑恢复 tool pairing 以获得更严格的消息结构校验。
177
210
 
178
211
  **同类问题影响与处理注意事项**
179
212
 
180
- - `enforce_anthropic_tool_pairing` 合成的 `is_error=True` 占位块只有 `tool_use_id`,同样需要 `id` 注入——辅助函数在配对后统一处理,无需在合成逻辑中单独添加。
181
- - `tool_result.id` 的值设为与 `tool_use_id` 相同,语义上可视为「内容块标识符」,对 zhipu 后端足够区分不同 tool_result 块。
213
+ - `NativeAnthropicVendor` 子类的自清理通道应**精确剪裁**:仅修复 vendor 自身拒绝的产物,不做跨供应商的全量清理。
214
+ - zhipu 后端出现新的 400 拒绝(如 inline tool_result 再次被拒),应优先调查是 zhipu 后端变更还是请求格式问题,而非立即加回 tool pairing(可能重新触发 500)。
215
+ - `_inject_tool_result_id_for_zhipu` 函数暂时保留在代码中(未删除),标记为 deprecated,待确认不需要后清理。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.3.1a5"
3
+ version = "0.3.1a7"
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"
@@ -352,6 +352,92 @@ def _inject_tool_result_id_for_zhipu(body: dict[str, Any]) -> int:
352
352
  return injected
353
353
 
354
354
 
355
+ def _extract_text_from_content(content: Any) -> str:
356
+ """从 tool_result 的 content 字段提取可读文本."""
357
+ if isinstance(content, str):
358
+ return content
359
+ if isinstance(content, list):
360
+ parts: list[str] = []
361
+ for item in content:
362
+ if isinstance(item, str):
363
+ parts.append(item)
364
+ elif isinstance(item, dict) and item.get("type") == "text":
365
+ parts.append(item.get("text", ""))
366
+ return " ".join(parts)
367
+ return ""
368
+
369
+
370
+ def _flatten_tool_blocks(body: dict[str, Any]) -> int:
371
+ """将 messages 中的 tool_use 和 tool_result 块转为 text 块.
372
+
373
+ zhipu GLM-5 后端的 ``ClaudeContentBlockToolResult`` 类缺少 ``id`` 属性,
374
+ 导致处理 tool_result 块时触发 ``AttributeError`` → HTTP 500。
375
+ 此函数将所有 tool_use / tool_result 块转为纯文本表示,
376
+ 让 zhipu 以普通文本对话处理,彻底规避反序列化缺陷。
377
+
378
+ Returns:
379
+ 被转换的 tool_use + tool_result 块总数。
380
+ """
381
+ import json as _json
382
+
383
+ converted = 0
384
+ for message in body.get("messages", []):
385
+ if not isinstance(message, dict):
386
+ continue
387
+ content = message.get("content")
388
+ if not isinstance(content, list):
389
+ continue
390
+
391
+ new_blocks: list[dict[str, Any]] = []
392
+ for block in content:
393
+ if not isinstance(block, dict):
394
+ new_blocks.append(block)
395
+ continue
396
+
397
+ block_type = block.get("type")
398
+
399
+ if block_type == "tool_use":
400
+ name = block.get("name", "unknown")
401
+ input_data = block.get("input", {})
402
+ try:
403
+ args_text = _json.dumps(input_data, ensure_ascii=False)
404
+ except (TypeError, ValueError):
405
+ args_text = str(input_data)
406
+ # 截断过长参数
407
+ if len(args_text) > 2000:
408
+ args_text = args_text[:1997] + "..."
409
+ new_blocks.append(
410
+ {"type": "text", "text": f"[Tool Call: {name}({args_text})]"}
411
+ )
412
+ converted += 1
413
+
414
+ elif block_type == "tool_result":
415
+ tool_use_id = block.get("tool_use_id", "?")
416
+ is_error = block.get("is_error", False)
417
+ result_text = _extract_text_from_content(block.get("content"))
418
+ if len(result_text) > 2000:
419
+ result_text = result_text[:1997] + "..."
420
+ prefix = "[ERROR] " if is_error else ""
421
+ new_blocks.append(
422
+ {
423
+ "type": "text",
424
+ "text": f"{prefix}[Tool Result for {tool_use_id}: {result_text}]",
425
+ }
426
+ )
427
+ converted += 1
428
+
429
+ else:
430
+ new_blocks.append(block)
431
+
432
+ # 如果 content 为空则插入占位
433
+ if not new_blocks:
434
+ new_blocks = [{"type": "text", "text": "..."}]
435
+
436
+ message["content"] = new_blocks
437
+
438
+ return converted
439
+
440
+
355
441
  def _strip_cache_control(body: dict[str, Any]) -> int:
356
442
  """从 system/messages/tools 中移除 cache_control 字段(就地).
357
443
 
@@ -572,13 +658,18 @@ def infer_source_vendor_from_body(body: dict[str, Any]) -> str | None:
572
658
  def prepare_copilot_to_zhipu(
573
659
  body: dict[str, Any],
574
660
  ) -> tuple[dict[str, Any], list[str]]:
575
- """copilot → zhipu 转换: 清理 copilot 产物以适配 GLM-5.
661
+ """copilot → zhipu 转换: 仅清理 copilot 产物中 zhipu 确认不支持的部分.
662
+
663
+ GLM-5 的 Anthropic 兼容端点:
664
+ - ✗ thinking / redacted_thinking 块 (signature 由非 Anthropic 签发)
665
+ - ✓ cache_control 字段 (cache_read 已在生产实证)
666
+ - ✓ tool_result 在 assistant 消息中内联 (zhipu 自身偶发产出,可自行消化)
667
+ - ✗ 顶层 thinking / extended_thinking 参数
576
668
 
577
- GLM-5 Anthropic 兼容端点对以下特性支持不完整:
578
- - thinking / redacted_thinking (signature 由非 Anthropic 签发)
579
- - cache_control 字段
580
- - 跨供应商产物 (misplaced tool_result, 非标准 tool_use ID)
581
- - 顶层 thinking / extended_thinking 参数
669
+ 注意: 不再执行 enforce_anthropic_tool_pairing 和 _inject_tool_result_id_for_zhipu。
670
+ 实证表明 tool_result 重定位会触发 zhipu 后端 ``'ClaudeContentBlockToolResult'
671
+ object has no attribute 'id'`` 500 错误;id 注入对 zhipu 的 Python 类
672
+ (不读取 JSON 中的 id 字段) 亦无效。详见 docs/issue.md。
582
673
 
583
674
  Returns:
584
675
  (prepared_body, adaptations) — adaptations 为应用的变换描述列表。
@@ -591,26 +682,16 @@ def prepare_copilot_to_zhipu(
591
682
  if stripped:
592
683
  adaptations.append(f"stripped_{stripped}_thinking_blocks")
593
684
 
594
- # Step 2: 移除 cache_control 字段
595
- removed_cc = _strip_cache_control(prepared)
596
- if removed_cc:
597
- adaptations.append(f"removed_{removed_cc}_cache_control_fields")
598
-
599
- # Step 3: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
685
+ # Step 2: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
600
686
  for param in ("thinking", "extended_thinking"):
601
687
  if param in prepared:
602
688
  del prepared[param]
603
689
  adaptations.append(f"removed_{param}_param")
604
690
 
605
- # Step 4: 强制 tool_use/tool_result 配对
606
- pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
607
- if pairing_fixes:
608
- adaptations.extend(pairing_fixes)
609
-
610
- # Step 5: 为 tool_result 块注入 id 字段(zhipu 后端 bug workaround)
611
- injected = _inject_tool_result_id_for_zhipu(prepared)
612
- if injected:
613
- adaptations.append(f"injected_{injected}_tool_result_id_fields")
691
+ # Step 3: 展平 tool_use/tool_result 为 text 块
692
+ flattened = _flatten_tool_blocks(prepared)
693
+ if flattened:
694
+ adaptations.append(f"flattened_{flattened}_tool_blocks")
614
695
 
615
696
  return prepared, adaptations
616
697
 
@@ -632,9 +713,11 @@ def prepare_anthropic_to_zhipu(
632
713
  Anthropic API 可能产生的非兼容产物:
633
714
  - ``server_tool_use`` blocks(web search / computer use 等 beta 功能)
634
715
  - ``thinking`` / ``redacted_thinking`` blocks(含 Anthropic 签发的 signature)
635
- - ``cache_control`` 字段
636
716
  - 顶层 ``thinking`` / ``extended_thinking`` 参数
637
717
 
718
+ 注意: 不再移除 cache_control (GLM-5 支持) ,不再执行 tool pairing 和
719
+ id 注入。原因同 prepare_copilot_to_zhipu 的 docstring。
720
+
638
721
  Returns:
639
722
  (prepared_body, adaptations) — adaptations 为应用的变换描述列表。
640
723
  """
@@ -651,26 +734,16 @@ def prepare_anthropic_to_zhipu(
651
734
  if stripped:
652
735
  adaptations.append(f"stripped_{stripped}_thinking_blocks")
653
736
 
654
- # Step 3: 移除 cache_control 字段
655
- removed_cc = _strip_cache_control(prepared)
656
- if removed_cc:
657
- adaptations.append(f"removed_{removed_cc}_cache_control_fields")
658
-
659
- # Step 4: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
737
+ # Step 3: 移除顶层 thinking/extended_thinking 参数(GLM-5 不支持)
660
738
  for param in ("thinking", "extended_thinking"):
661
739
  if param in prepared:
662
740
  del prepared[param]
663
741
  adaptations.append(f"removed_{param}_param")
664
742
 
665
- # Step 5: 强制 tool_use/tool_result 配对
666
- pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
667
- if pairing_fixes:
668
- adaptations.extend(pairing_fixes)
669
-
670
- # Step 6: 为 tool_result 块注入 id 字段(zhipu 后端 bug workaround)
671
- injected = _inject_tool_result_id_for_zhipu(prepared)
672
- if injected:
673
- adaptations.append(f"injected_{injected}_tool_result_id_fields")
743
+ # Step 4: 展平 tool_use/tool_result 为 text 块
744
+ flattened = _flatten_tool_blocks(prepared)
745
+ if flattened:
746
+ adaptations.append(f"flattened_{flattened}_tool_blocks")
674
747
 
675
748
  return prepared, adaptations
676
749
 
@@ -782,25 +855,22 @@ def prepare_zhipu_to_anthropic(
782
855
  def prepare_zhipu_self_cleanup(
783
856
  body: dict[str, Any],
784
857
  ) -> tuple[dict[str, Any], list[str]]:
785
- """zhipu → zhipu 自清理: 仅修复 zhipu 自身无法消化的产物.
858
+ """zhipu → zhipu 自清理: 仅剥离 zhipu 自身的流式残块.
786
859
 
787
- GLM-5 偶发地在 assistant 消息中输出 ``tool_result`` 块(违反 Anthropic 规范),
788
- 或在流式响应中暴露 ``server_tool_use_delta`` 私有块。当 Claude Code 将这些
789
- 产物原样回送下一轮请求时,zhipu 的 Anthropic 兼容端点会以 400 拒绝
790
- (表现为 "400 + tool_results" 偶发,进而触发到 copilot 的降级)。
860
+ GLM-5 在流式响应中偶发暴露 ``server_tool_use_delta`` 私有块。当 Claude Code
861
+ 将这些产物原样回送下一轮请求时,zhipu Anthropic 兼容端点会拒绝。
791
862
 
792
- 本通道仅修复 zhipu 自身拒绝的两类产物,**保留** 所有 zhipu 原生支持的特性:
863
+ 本通道**保留**所有 zhipu 原生支持的特性:
793
864
 
794
865
  - ✓ ``srvtoolu_*`` ID 与 ``server_tool_use`` 类型(zhipu 原生)
795
866
  - ✓ thinking blocks 的 zhipu 自签 signature
796
867
  - ✓ ``cache_control`` 字段(GLM Anthropic 端点支持,cache_read 已实证)
797
868
  - ✓ 顶层 ``thinking`` / ``extended_thinking`` 参数
869
+ - ✓ tool_result 在 assistant 消息中内联(zhipu 自身偶发产出,可自行消化)
798
870
 
799
- 清理操作(顺序、就地、幂等):
800
- 1. 剥离 ``server_tool_use_delta`` 流式残块
801
- 2. 强制 tool_use/tool_result 配对(关键: 把 assistant 内联的 tool_result
802
- 搬迁到紧随的 user 消息)
803
- 3. 为 ``tool_result`` 块注入 ``id`` 字段(zhipu 后端错误访问 ``.id`` 属性)
871
+ 注意: 不再执行 enforce_anthropic_tool_pairing 和 _inject_tool_result_id_for_zhipu。
872
+ 实证表明 tool_result 重定位会触发 zhipu 后端 500 错误。
873
+ 详见 docs/issue.md。
804
874
 
805
875
  Returns:
806
876
  (prepared_body, adaptations) — adaptations 为应用的变换描述列表。
@@ -813,15 +883,10 @@ def prepare_zhipu_self_cleanup(
813
883
  if removed_vendor_blocks:
814
884
  adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
815
885
 
816
- # Step 2: 强制 tool_use/tool_result 配对
817
- pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
818
- if pairing_fixes:
819
- adaptations.extend(pairing_fixes)
820
-
821
- # Step 3: 为 tool_result 块注入 id 字段(zhipu 后端 bug workaround)
822
- injected = _inject_tool_result_id_for_zhipu(prepared)
823
- if injected:
824
- adaptations.append(f"injected_{injected}_tool_result_id_fields")
886
+ # Step 2: 展平 tool_use/tool_result 为 text 块
887
+ flattened = _flatten_tool_blocks(prepared)
888
+ if flattened:
889
+ adaptations.append(f"flattened_{flattened}_tool_blocks")
825
890
 
826
891
  return prepared, adaptations
827
892
 
@@ -99,6 +99,7 @@ class CapabilityLossReason(Enum):
99
99
  IMAGES = "images"
100
100
  VENDOR_TOOLS = "vendor_tools"
101
101
  METADATA = "metadata"
102
+ TOOL_RESULTS = "tool_results"
102
103
 
103
104
 
104
105
  @dataclass(frozen=True)
@@ -109,6 +110,7 @@ class RequestCapabilities:
109
110
  has_thinking: bool = False
110
111
  has_images: bool = False
111
112
  has_metadata: bool = False
113
+ has_tool_results: bool = False
112
114
 
113
115
 
114
116
  @dataclass(frozen=True)
@@ -111,15 +111,22 @@ def is_semantic_rejection(
111
111
  def build_request_capabilities(body: dict[str, Any]) -> RequestCapabilities:
112
112
  """从请求体提取能力画像."""
113
113
  has_images = False
114
+ has_tool_results = False
114
115
  for msg in body.get("messages", []):
115
116
  content = msg.get("content")
116
117
  if not isinstance(content, list):
117
118
  continue
118
- if any(
119
- isinstance(block, dict) and block.get("type") == "image"
120
- for block in content
121
- ):
122
- has_images = True
119
+ for block in content:
120
+ if not isinstance(block, dict):
121
+ continue
122
+ block_type = block.get("type")
123
+ if block_type == "image" and not has_images:
124
+ has_images = True
125
+ elif block_type == "tool_result" and not has_tool_results:
126
+ has_tool_results = True
127
+ if has_images and has_tool_results:
128
+ break
129
+ if has_images and has_tool_results:
123
130
  break
124
131
 
125
132
  return RequestCapabilities(
@@ -127,4 +134,5 @@ def build_request_capabilities(body: dict[str, Any]) -> RequestCapabilities:
127
134
  has_thinking=bool(body.get("thinking") or body.get("extended_thinking")),
128
135
  has_images=has_images,
129
136
  has_metadata=bool(body.get("metadata")),
137
+ has_tool_results=has_tool_results,
130
138
  )
@@ -362,3 +362,75 @@ def test_string_content_not_image():
362
362
  def test_empty_messages():
363
363
  caps = build_request_capabilities({"model": "m", "messages": []})
364
364
  assert caps.has_images is False
365
+ assert caps.has_tool_results is False
366
+
367
+
368
+ def test_tool_results_in_user_message():
369
+ caps = build_request_capabilities(
370
+ {
371
+ "model": "m",
372
+ "messages": [
373
+ {
374
+ "role": "user",
375
+ "content": [
376
+ {
377
+ "type": "tool_result",
378
+ "tool_use_id": "toolu_1",
379
+ "content": "ok",
380
+ }
381
+ ],
382
+ }
383
+ ],
384
+ }
385
+ )
386
+ assert caps.has_tool_results is True
387
+
388
+
389
+ def test_tool_results_in_assistant_message():
390
+ caps = build_request_capabilities(
391
+ {
392
+ "model": "m",
393
+ "messages": [
394
+ {
395
+ "role": "assistant",
396
+ "content": [
397
+ {
398
+ "type": "tool_use",
399
+ "id": "toolu_1",
400
+ "name": "bash",
401
+ "input": {},
402
+ },
403
+ {
404
+ "type": "tool_result",
405
+ "tool_use_id": "toolu_1",
406
+ "content": "ok",
407
+ },
408
+ ],
409
+ }
410
+ ],
411
+ }
412
+ )
413
+ assert caps.has_tool_results is True
414
+
415
+
416
+ def test_no_tool_results():
417
+ caps = build_request_capabilities(
418
+ {
419
+ "model": "m",
420
+ "messages": [
421
+ {"role": "user", "content": "hello"},
422
+ {
423
+ "role": "assistant",
424
+ "content": [
425
+ {
426
+ "type": "tool_use",
427
+ "id": "toolu_1",
428
+ "name": "bash",
429
+ "input": {},
430
+ }
431
+ ],
432
+ },
433
+ ],
434
+ }
435
+ )
436
+ assert caps.has_tool_results is False
@@ -115,13 +115,20 @@ class TestCapabilityLossReason:
115
115
  assert CapabilityLossReason.METADATA.value == "metadata"
116
116
 
117
117
  def test_member_count(self):
118
- """枚举成员数量固定为 5."""
119
- assert len(CapabilityLossReason) == 5
118
+ """枚举成员数量."""
119
+ assert len(CapabilityLossReason) == 6
120
120
 
121
121
  def test_can_iterate(self):
122
122
  """可遍历所有成员."""
123
123
  names = {m.name for m in CapabilityLossReason}
124
- assert names == {"TOOLS", "THINKING", "IMAGES", "VENDOR_TOOLS", "METADATA"}
124
+ assert names == {
125
+ "TOOLS",
126
+ "THINKING",
127
+ "IMAGES",
128
+ "VENDOR_TOOLS",
129
+ "METADATA",
130
+ "TOOL_RESULTS",
131
+ }
125
132
 
126
133
  def test_lookup_by_value(self):
127
134
  """可通过 value 反查成员."""
@@ -144,6 +151,7 @@ class TestRequestCapabilities:
144
151
  assert caps.has_thinking is False
145
152
  assert caps.has_images is False
146
153
  assert caps.has_metadata is False
154
+ assert caps.has_tool_results is False
147
155
 
148
156
  def test_custom_true_values(self):
149
157
  """自定义构造: 指定 True 的字段正确赋值."""
@@ -152,6 +160,7 @@ class TestRequestCapabilities:
152
160
  assert caps.has_images is True
153
161
  assert caps.has_thinking is False
154
162
  assert caps.has_metadata is False
163
+ assert caps.has_tool_results is False
155
164
 
156
165
  def test_frozen_immutable(self):
157
166
  """frozen dataclass: 赋值操作抛 AttributeError."""
@@ -2034,7 +2034,7 @@ class TestPrepareBodyForTierSelfTransition:
2034
2034
  """验证 zhipu → zhipu 自转换通道在 _prepare_body_for_tier 中的应用行为."""
2035
2035
 
2036
2036
  def test_applies_zhipu_self_cleanup(self):
2037
- """source=zhipu, target=zhipu → 剥离 server_tool_use_delta + tool pairing."""
2037
+ """source=zhipu, target=zhipu → 剥离 server_tool_use_delta 并展平 tool 块."""
2038
2038
  tier = MagicMock()
2039
2039
  tier.name = "zhipu"
2040
2040
 
@@ -2067,17 +2067,12 @@ class TestPrepareBodyForTierSelfTransition:
2067
2067
  assert result is not body
2068
2068
  assert len(body["messages"][0]["content"]) == 3
2069
2069
 
2070
- # delta 块被剥离, tool_result 被搬迁出 assistant
2070
+ # delta 块被剥离
2071
2071
  assistant_content = result["messages"][0]["content"]
2072
+ assert all(b.get("type") != "server_tool_use_delta" for b in assistant_content)
2073
+ # tool_use 和 tool_result 被展平为 text
2072
2074
  assert all(
2073
- b.get("type") not in ("server_tool_use_delta", "tool_result")
2074
- for b in assistant_content
2075
- )
2076
- # tool_result 已搬到下一个 user 消息
2077
- assert result["messages"][1]["role"] == "user"
2078
- assert any(
2079
- b.get("type") == "tool_result" and b.get("tool_use_id") == "srvtoolu_a"
2080
- for b in result["messages"][1]["content"]
2075
+ b.get("type") not in ("tool_use", "tool_result") for b in assistant_content
2081
2076
  )
2082
2077
 
2083
2078
  def test_self_cleanup_preserves_srvtoolu_ids(self):
@@ -67,6 +67,7 @@ def test_request_capabilities_defaults():
67
67
  assert caps.has_thinking is False
68
68
  assert caps.has_images is False
69
69
  assert caps.has_metadata is False
70
+ assert caps.has_tool_results is False
70
71
 
71
72
 
72
73
  def test_request_capabilities_immutable():