coding-proxy 0.3.1a6__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.1a6 → coding_proxy-0.3.1a7}/PKG-INFO +1 -1
  2. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/issue.md +20 -14
  3. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/pyproject.toml +1 -1
  4. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/vendor_channels.py +101 -0
  5. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/vendor.py +2 -0
  6. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/error_classifier.py +13 -5
  7. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_error_classifier.py +72 -0
  8. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_vendor.py +12 -3
  9. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_router_executor.py +4 -12
  10. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_types.py +1 -0
  11. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_vendor_channels.py +54 -60
  12. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_vendors.py +36 -2
  13. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/uv.lock +1 -1
  14. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.github/workflows/ci.yml +0 -0
  15. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.github/workflows/coverage.yml +0 -0
  16. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.github/workflows/release.yml +0 -0
  17. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.gitignore +0 -0
  18. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/.pre-commit-config.yaml +0 -0
  19. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/AGENTS.md +0 -0
  20. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/CHANGELOG.md +0 -0
  21. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/CLAUDE.md +0 -0
  22. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/LICENSE +0 -0
  23. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/README.md +0 -0
  24. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/assets/dashboard-v0.2.4.png +0 -0
  25. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/config-reference.md +0 -0
  26. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/convert.md +0 -0
  27. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/design-patterns.md +0 -0
  28. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/routing.md +0 -0
  29. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/testing.md +0 -0
  30. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/arch/vendors.md +0 -0
  31. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/ci-cd.md +0 -0
  32. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/framework.md +0 -0
  33. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/api-reference.md +0 -0
  34. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/cli-reference.md +0 -0
  35. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/dashboard.md +0 -0
  36. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/monitoring.md +0 -0
  37. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/quickstart.md +0 -0
  38. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/guide/vendors.md +0 -0
  39. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/user-guide.md +0 -0
  40. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/docs/zh-CN/README.md +0 -0
  41. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/__init__.py +0 -0
  42. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/__init__.py +0 -0
  43. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/__main__.py +0 -0
  44. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/__init__.py +0 -0
  45. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/__init__.py +0 -0
  46. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/base.py +0 -0
  47. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/github.py +0 -0
  48. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/providers/google.py +0 -0
  49. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/runtime.py +0 -0
  50. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/auth/store.py +0 -0
  51. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/__init__.py +0 -0
  52. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/auth_commands.py +0 -0
  53. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/cli/banner.py +0 -0
  54. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/__init__.py +0 -0
  55. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/canonical.py +0 -0
  56. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/compat/session_store.py +0 -0
  57. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/__init__.py +0 -0
  58. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/auth_schema.py +0 -0
  59. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/config.default.yaml +0 -0
  60. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/loader.py +0 -0
  61. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/resiliency.py +0 -0
  62. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/routing.py +0 -0
  63. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/schema.py +0 -0
  64. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/server.py +0 -0
  65. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/config/vendors.py +0 -0
  66. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/__init__.py +0 -0
  67. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  68. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  69. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  70. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  71. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  72. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/__init__.py +0 -0
  73. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/db.py +0 -0
  74. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/formatters.py +0 -0
  75. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/logging/stats.py +0 -0
  76. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/__init__.py +0 -0
  77. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/auth.py +0 -0
  78. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/compat.py +0 -0
  79. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/constants.py +0 -0
  80. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/pricing.py +0 -0
  81. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/model/token.py +0 -0
  82. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/__init__.py +0 -0
  83. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/config.py +0 -0
  84. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  85. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  86. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  87. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  88. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/handler.py +0 -0
  89. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/operation.py +0 -0
  90. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/routes.py +0 -0
  91. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/native_api/usage_registry.py +0 -0
  92. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/pricing.py +0 -0
  93. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/__init__.py +0 -0
  94. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  95. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/executor.py +0 -0
  96. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/model_mapper.py +0 -0
  97. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/quota_guard.py +0 -0
  98. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/rate_limit.py +0 -0
  99. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/retry.py +0 -0
  100. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/router.py +0 -0
  101. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/session_manager.py +0 -0
  102. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/tier.py +0 -0
  103. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_parser.py +0 -0
  104. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/routing/usage_recorder.py +0 -0
  105. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/__init__.py +0 -0
  106. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/app.py +0 -0
  107. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/dashboard.py +0 -0
  108. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/factory.py +0 -0
  109. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/responses.py +0 -0
  110. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/server/routes.py +0 -0
  111. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/__init__.py +0 -0
  112. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  113. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/__init__.py +0 -0
  114. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/alibaba.py +0 -0
  115. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/anthropic.py +0 -0
  116. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/antigravity.py +0 -0
  117. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/base.py +0 -0
  118. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot.py +0 -0
  119. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_models.py +0 -0
  120. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  121. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  122. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/doubao.py +0 -0
  123. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/kimi.py +0 -0
  124. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/minimax.py +0 -0
  125. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/mixins.py +0 -0
  126. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  127. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/token_manager.py +0 -0
  128. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/xiaomi.py +0 -0
  129. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/src/coding/proxy/vendors/zhipu.py +0 -0
  130. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/__init__.py +0 -0
  131. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_antigravity.py +0 -0
  132. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_app_routes.py +0 -0
  133. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_auto_login.py +0 -0
  134. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_banner.py +0 -0
  135. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_circuit_breaker.py +0 -0
  136. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_cli_usage.py +0 -0
  137. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_compat.py +0 -0
  138. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_config_init.py +0 -0
  139. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_config_loader.py +0 -0
  140. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_convert_request.py +0 -0
  141. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_convert_response.py +0 -0
  142. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_convert_sse.py +0 -0
  143. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot.py +0 -0
  144. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_request.py +0 -0
  145. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_convert_response.py +0 -0
  146. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_models.py +0 -0
  147. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_copilot_urls.py +0 -0
  148. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_currency.py +0 -0
  149. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_logging_dual_write.py +0 -0
  150. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_mixins.py +0 -0
  151. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_auth.py +0 -0
  152. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_compat.py +0 -0
  153. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_constants.py +0 -0
  154. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_mapper.py +0 -0
  155. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_pricing.py +0 -0
  156. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_model_token.py +0 -0
  157. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_base_url_override.py +0 -0
  158. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_extractors.py +0 -0
  159. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_handler.py +0 -0
  160. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_operation.py +0 -0
  161. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_api_routes.py +0 -0
  162. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_native_vendors.py +0 -0
  163. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_parse_usage.py +0 -0
  164. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_parse_usage_gemini.py +0 -0
  165. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_pricing.py +0 -0
  166. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_quota_guard.py +0 -0
  167. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_rate_limit.py +0 -0
  168. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_router_chain.py +0 -0
  169. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_runtime_reauth.py +0 -0
  170. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_schema.py +0 -0
  171. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_streaming_anthropic_compat.py +0 -0
  172. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_tier.py +0 -0
  173. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_tiers_config.py +0 -0
  174. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_time_range.py +0 -0
  175. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_token_logger.py +0 -0
  176. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_token_logger_native_columns.py +0 -0
  177. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_token_manager.py +0 -0
  178. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_vendor_streaming.py +0 -0
  179. {coding_proxy-0.3.1a6 → coding_proxy-0.3.1a7}/tests/test_zhipu.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.3.1a6
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
@@ -158,29 +158,35 @@ WARNING zhipu stream error: status=500 body='...message":"\'ClaudeContentBlockTo
158
158
 
159
159
  zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id` 字段。
160
160
 
161
- **根因**(2026-04-29 复盘更新)
161
+ **根因**(2026-04-29 第二次复盘更新)
162
162
 
163
- **初始诊断**(已推翻):认为 zhipu 后端期望 `tool_result` `id` 字段,通过 `_inject_tool_result_id_for_zhipu` 注入 `id = tool_use_id` 可绕过。
163
+ **第一次诊断**(已推翻):认为 `_inject_tool_result_id_for_zhipu` 注入 `id` 可绕过。实证:注入 114 个块后 500 依旧。
164
164
 
165
- **实际根因**:转换通道本身引入的问题。具体因果链:
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
166
 
167
- 1. **转换前**:zhipu 偶发在 assistant 消息中内联输出 `tool_result`(违反 Anthropic 规范),但 zhipu 后端对 assistant 消息中内联的 `tool_result` **不做 `.id` 属性访问**,因此不触发 500。
168
- 2. **转换后**:所有 zhipu 目标通道执行 `enforce_anthropic_tool_pairing`,将 assistant 内联的 `tool_result` 搬迁到紧随的 user 消息。zhipu 后端对 user 消息中的 `tool_result` **执行 `.id` 属性访问**(代码路径不同),触发 `AttributeError` → 500。
169
- 3. **`_inject_tool_result_id_for_zhipu` 无效**:该函数往 JSON dict 注入 `"id": tool_use_id`,但 zhipu 后端的 `ClaudeContentBlockToolResult` Python 类不从 JSON 读取 `id` 字段(类定义中无此属性),注入的值在反序列化时被丢弃。
167
+ **实际根因**:zhipu 后端的 `ClaudeContentBlockToolResult` Python 类**没有 `id` 属性**,但 zhipu 代码在处理**所有** `tool_result` 块时都访问 `obj.id`,无论块位于 assistant 还是 user 消息。三层因果链:
170
168
 
171
- **实证依据**:用户确认「转换通道之前 zhipu 正常,转换通道之后才出现 500 错误」。
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
172
 
173
- **处理方式**(2026-04-29 更新)
173
+ **实证依据**:
174
+ - 有注入(114 个块)→ 500;无注入 → 500。结论:注入无效。
175
+ - 有 tool pairing → 500;无 tool pairing → 500。结论:tool pairing 不是触发条件。
176
+ - 首次请求(无 tool_result 块)→ zhipu 正常。结论:500 由 tool_result 块本身触发。
174
177
 
175
- 从所有 zhipu 目标转换通道中移除以下三个步骤:
178
+ **处理方式**(2026-04-29 第二次更新)
176
179
 
177
- | 移除项 | 原因 |
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
+ | 变更项 | 说明 |
178
183
  |--------|------|
179
- | `enforce_anthropic_tool_pairing` | 搬迁 `tool_result` user 消息触发 zhipu 500 |
180
- | `_inject_tool_result_id_for_zhipu` | zhipu 类不读取注入的 `id`,无效且可能干扰 |
181
- | `_strip_cache_control` | zhipu 原生支持 `cache_control`(cache_read 已实证),剥离反损性能 |
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 块检测逻辑 |
182
188
 
183
- 保留的必要步骤:
189
+ 保留的 zhipu 目标转换通道精简步骤:
184
190
 
185
191
  | 保留项 | 原因 |
186
192
  |--------|------|
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.3.1a6"
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
 
@@ -602,6 +688,11 @@ def prepare_copilot_to_zhipu(
602
688
  del prepared[param]
603
689
  adaptations.append(f"removed_{param}_param")
604
690
 
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")
695
+
605
696
  return prepared, adaptations
606
697
 
607
698
 
@@ -649,6 +740,11 @@ def prepare_anthropic_to_zhipu(
649
740
  del prepared[param]
650
741
  adaptations.append(f"removed_{param}_param")
651
742
 
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")
747
+
652
748
  return prepared, adaptations
653
749
 
654
750
 
@@ -787,6 +883,11 @@ def prepare_zhipu_self_cleanup(
787
883
  if removed_vendor_blocks:
788
884
  adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
789
885
 
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")
890
+
790
891
  return prepared, adaptations
791
892
 
792
893
 
@@ -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,12 +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.
2038
-
2039
- 不再做 tool pairing(搬迁 tool_result 会触发 zhipu 500),
2040
- 也不做 id 注入(zhipu 类不读取 JSON 中的 id)。
2041
- inline tool_result 保留在 assistant 消息中,zhipu 可自行消化。
2042
- """
2037
+ """source=zhipu, target=zhipu → 剥离 server_tool_use_delta 并展平 tool 块."""
2043
2038
  tier = MagicMock()
2044
2039
  tier.name = "zhipu"
2045
2040
 
@@ -2075,13 +2070,10 @@ class TestPrepareBodyForTierSelfTransition:
2075
2070
  # delta 块被剥离
2076
2071
  assistant_content = result["messages"][0]["content"]
2077
2072
  assert all(b.get("type") != "server_tool_use_delta" for b in assistant_content)
2078
- # inline tool_result 保留在 assistant 中(不再搬迁)
2079
- assert any(
2080
- b.get("type") == "tool_result" and b.get("tool_use_id") == "srvtoolu_a"
2081
- for b in assistant_content
2073
+ # tool_use tool_result 被展平为 text
2074
+ assert all(
2075
+ b.get("type") not in ("tool_use", "tool_result") for b in assistant_content
2082
2076
  )
2083
- # 不应插入额外的 user 消息
2084
- assert len(result["messages"]) == 1
2085
2077
 
2086
2078
  def test_self_cleanup_preserves_srvtoolu_ids(self):
2087
2079
  """回归保护: 自清理通道不得改写 zhipu 原生 srvtoolu_* ID."""
@@ -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():
@@ -268,8 +268,8 @@ class TestCopilotToZhipuChannel:
268
268
  assert "removed_thinking_param" in adaptations
269
269
  assert "removed_extended_thinking_param" in adaptations
270
270
 
271
- def test_does_not_relocate_tool_results(self):
272
- """copilot → zhipu 不再执行 tool pairing(避免触发 zhipu 500)."""
271
+ def test_flattens_tool_use_blocks(self):
272
+ """copilot → zhipu tool_use 展平为 text 块."""
273
273
  body = {
274
274
  "messages": [
275
275
  {
@@ -279,7 +279,7 @@ class TestCopilotToZhipuChannel:
279
279
  "type": "tool_use",
280
280
  "id": "toolu_1",
281
281
  "name": "bash",
282
- "input": {},
282
+ "input": {"command": "ls"},
283
283
  },
284
284
  ],
285
285
  },
@@ -290,15 +290,15 @@ class TestCopilotToZhipuChannel:
290
290
  ],
291
291
  }
292
292
  prepared, adaptations = prepare_copilot_to_zhipu(body)
293
- # user 消息内容不变(无 synthesized tool_result)
294
- user_content = prepared["messages"][1]["content"]
295
- tool_results = [
296
- b
297
- for b in user_content
298
- if isinstance(b, dict) and b.get("type") == "tool_result"
299
- ]
300
- assert len(tool_results) == 0
301
- assert not any("misplaced" in a for a in adaptations)
293
+ assert any("flattened" in a for a in adaptations)
294
+ # tool_use 被转为 text
295
+ assistant_content = prepared["messages"][0]["content"]
296
+ assert all(b.get("type") != "tool_use" for b in assistant_content)
297
+ assert any(
298
+ "Tool Call: bash" in b.get("text", "")
299
+ for b in assistant_content
300
+ if b.get("type") == "text"
301
+ )
302
302
 
303
303
  def test_combined_transformations(self):
304
304
  body = {
@@ -333,14 +333,9 @@ class TestCopilotToZhipuChannel:
333
333
  # cache_control 保留
334
334
  assert "cache_control" in prepared["system"][0]
335
335
  assert "thinking" not in prepared
336
- # tool pairing 不执行(user 消息内容不变)
337
- user_content = prepared["messages"][1]["content"]
338
- tool_results = [
339
- b
340
- for b in user_content
341
- if isinstance(b, dict) and b.get("type") == "tool_result"
342
- ]
343
- assert len(tool_results) == 0
336
+ # tool_use 被展平为 text
337
+ assistant_content = prepared["messages"][0]["content"]
338
+ assert all(b.get("type") != "tool_use" for b in assistant_content)
344
339
 
345
340
  def test_preserves_original_body(self):
346
341
  body = {
@@ -397,8 +392,8 @@ class TestCopilotToZhipuChannel:
397
392
  assert prepared2 == prepared1
398
393
  assert adaptations2 == []
399
394
 
400
- def test_no_id_injection_on_tool_result(self):
401
- """copilot → zhipu 转换不再注入 id 字段(zhipu 类不读取,注入无效)."""
395
+ def test_flattens_tool_result_in_user_message(self):
396
+ """copilot → zhipu user 消息中的 tool_result 展平为 text."""
402
397
  body = {
403
398
  "messages": [
404
399
  {
@@ -425,9 +420,10 @@ class TestCopilotToZhipuChannel:
425
420
  ],
426
421
  }
427
422
  prepared, adaptations = prepare_copilot_to_zhipu(body)
428
- tr = prepared["messages"][1]["content"][0]
429
- assert "id" not in tr
430
- assert not any("injected" in a for a in adaptations)
423
+ # tool_result 被展平为 text
424
+ user_content = prepared["messages"][1]["content"]
425
+ assert all(b.get("type") != "tool_result" for b in user_content)
426
+ assert any("flattened" in a for a in adaptations)
431
427
 
432
428
 
433
429
  # ── zhipu → anthropic 转换通道测试 ────────────────────────────────
@@ -764,7 +760,7 @@ class TestZhipuSelfCleanupChannel:
764
760
  assert any("zhipu_vendor_blocks" in a for a in adaptations)
765
761
 
766
762
  def test_preserves_inline_tool_result_in_assistant(self):
767
- """assistant 内联 tool_result 保留原位(不再搬迁,避免触发 zhipu 500)."""
763
+ """assistant 内联 tool_result tool_use 被展平为 text 块."""
768
764
  body = {
769
765
  "messages": [
770
766
  {
@@ -788,19 +784,15 @@ class TestZhipuSelfCleanupChannel:
788
784
  }
789
785
  prepared, adaptations = prepare_zhipu_self_cleanup(body)
790
786
 
791
- # assistant 消息中 tool_result 保留原位
787
+ # tool_use tool_result 均被展平为 text
792
788
  assistant_content = prepared["messages"][0]["content"]
793
- assert any(
794
- b.get("type") == "tool_result" and b.get("tool_use_id") == "srvtoolu_a"
795
- for b in assistant_content
789
+ assert all(
790
+ b.get("type") not in ("tool_use", "tool_result") for b in assistant_content
796
791
  )
797
- # 不应有 tool pairing 相关的 adaptations
798
- assert not any("misplaced" in a for a in adaptations)
799
- assert not any("orphaned" in a for a in adaptations)
800
- assert not any("injected" in a for a in adaptations)
792
+ assert any("flattened" in a for a in adaptations)
801
793
 
802
- def test_no_id_injection(self):
803
- """自清理通道不再注入 id 字段(zhipu 类不读取,注入无效)."""
794
+ def test_tool_result_flattened_to_text(self):
795
+ """自清理通道将 tool_result 展平为 text 块."""
804
796
  body = {
805
797
  "messages": [
806
798
  {
@@ -827,13 +819,13 @@ class TestZhipuSelfCleanupChannel:
827
819
  ],
828
820
  }
829
821
  prepared, adaptations = prepare_zhipu_self_cleanup(body)
822
+ # tool_result 被展平为 text
823
+ user_content = prepared["messages"][1]["content"]
824
+ assert all(b.get("type") != "tool_result" for b in user_content)
825
+ assert any("flattened" in a for a in adaptations)
830
826
 
831
- tr = prepared["messages"][1]["content"][0]
832
- assert "id" not in tr
833
- assert not any("injected" in a for a in adaptations)
834
-
835
- def test_preserves_existing_id(self):
836
- """tool_result 已有 id 字段时应原样保留,不被修改."""
827
+ def test_flattens_tool_result_with_existing_id(self):
828
+ """自清理通道将含 id tool_result 也展平为 text."""
837
829
  body = {
838
830
  "messages": [
839
831
  {
@@ -861,9 +853,10 @@ class TestZhipuSelfCleanupChannel:
861
853
  ],
862
854
  }
863
855
  prepared, adaptations = prepare_zhipu_self_cleanup(body)
864
- tr = prepared["messages"][1]["content"][0]
865
- assert tr["id"] == "original_id"
866
- assert not any("injected" in a for a in adaptations)
856
+ # tool_result 被展平为 text,不再保留原结构
857
+ user_content = prepared["messages"][1]["content"]
858
+ assert all(b.get("type") != "tool_result" for b in user_content)
859
+ assert any("flattened" in a for a in adaptations)
867
860
 
868
861
  def test_preserves_srvtoolu_ids(self):
869
862
  """zhipu 原生 srvtoolu_* ID 与 server_tool_use 类型必须保留."""
@@ -1074,25 +1067,25 @@ class TestZhipuSelfCleanupChannel:
1074
1067
  assistant_content = prepared["messages"][0]["content"]
1075
1068
  # delta 被剥离
1076
1069
  assert all(b.get("type") != "server_tool_use_delta" for b in assistant_content)
1077
- # inline tool_result 保留在 assistant 中(不再搬迁)
1078
- assert any(
1079
- b.get("type") == "tool_result" and b.get("tool_use_id") == "toolu_bash_001"
1080
- for b in assistant_content
1081
- )
1070
+ # tool_use / tool_result flatten 为 text 块
1071
+ assert not any(b.get("type") == "tool_use" for b in assistant_content)
1072
+ assert not any(b.get("type") == "tool_result" for b in assistant_content)
1082
1073
  # server_tool_use 与其 srvtoolu_* ID 完整保留
1083
1074
  srv_block = next(
1084
1075
  b for b in assistant_content if b.get("type") == "server_tool_use"
1085
1076
  )
1086
1077
  assert srv_block["id"] == "srvtoolu_native"
1087
- # tool_use ID 同样保留
1088
- tool_use_block = next(
1089
- b for b in assistant_content if b.get("type") == "tool_use"
1090
- )
1091
- assert tool_use_block["id"] == "toolu_bash_001"
1078
+ # flatten 后应包含 tool_use tool_result 对应的 text 块
1079
+ text_contents = [
1080
+ b.get("text", "") for b in assistant_content if b.get("type") == "text"
1081
+ ]
1082
+ assert any("Tool Call: bash" in t for t in text_contents)
1083
+ assert any("Tool Result for toolu_bash_001" in t for t in text_contents)
1092
1084
  # 不插入额外 user 消息
1093
1085
  assert len(prepared["messages"]) == 1
1094
1086
  # 关键 adaptation 标签
1095
1087
  assert any("zhipu_vendor_blocks" in a for a in adaptations)
1088
+ assert any("flattened" in a and "tool_blocks" in a for a in adaptations)
1096
1089
  # 不应有 tool pairing / id 注入 相关 adaptation
1097
1090
  assert not any("misplaced" in a for a in adaptations)
1098
1091
  assert not any("injected" in a for a in adaptations)
@@ -2858,8 +2851,8 @@ class TestAnthropicToZhipuChannel:
2858
2851
  assert any("server_tool_use" in a for a in adaptations)
2859
2852
  assert any("thinking_blocks" in a for a in adaptations)
2860
2853
 
2861
- def test_no_id_injection_on_tool_result(self):
2862
- """anthropic → zhipu 转换不再注入 id 字段(zhipu 类不读取,注入无效)."""
2854
+ def test_flattens_tool_result_in_user_message(self):
2855
+ """anthropic → zhipu tool_result 展平为 text 块."""
2863
2856
  body = {
2864
2857
  "messages": [
2865
2858
  {
@@ -2886,6 +2879,7 @@ class TestAnthropicToZhipuChannel:
2886
2879
  ],
2887
2880
  }
2888
2881
  prepared, adaptations = prepare_anthropic_to_zhipu(body)
2889
- tr = prepared["messages"][1]["content"][0]
2890
- assert "id" not in tr
2891
- assert not any("injected" in a for a in adaptations)
2882
+ # tool_result 被展平为 text
2883
+ user_content = prepared["messages"][1]["content"]
2884
+ assert all(b.get("type") != "tool_result" for b in user_content)
2885
+ assert any("flattened" in a for a in adaptations)