coding-proxy 0.5.1a2__tar.gz → 0.5.1a4__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.1a2 → coding_proxy-0.5.1a4}/AGENTS.md +4 -4
  2. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/PKG-INFO +1 -1
  3. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/pyproject.toml +1 -1
  4. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/config.default.yaml +6 -0
  5. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/vendor_channels.py +125 -1
  6. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/executor.py +2 -2
  7. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/server/dashboard.py +19 -2
  8. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/server/routes.py +7 -3
  9. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/e2e/test_e2e_http.py +5 -2
  10. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_app_routes.py +5 -4
  11. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_router_executor.py +10 -1
  12. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_vendor_channels.py +218 -0
  13. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/uv.lock +1 -1
  14. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/.github/workflows/ci.yml +0 -0
  15. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/.github/workflows/coverage.yml +0 -0
  16. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/.github/workflows/release.yml +0 -0
  17. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/.gitignore +0 -0
  18. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/.pre-commit-config.yaml +0 -0
  19. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/CHANGELOG.md +0 -0
  20. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/CLAUDE.md +0 -0
  21. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/LICENSE +0 -0
  22. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/README.md +0 -0
  23. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/assets/dashboard-v0.4.0.png +0 -0
  24. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/assets/model-calling-v0.5.0.png +0 -0
  25. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/assets/session-v0.4.0.png +0 -0
  26. {coding_proxy-0.5.1a2/docs/agents → coding_proxy-0.5.1a4/docs/.agents}/browser-validation.md +0 -0
  27. {coding_proxy-0.5.1a2/docs/agents → coding_proxy-0.5.1a4/docs/.agents}/issue.md +0 -0
  28. {coding_proxy-0.5.1a2/docs/agents → coding_proxy-0.5.1a4/docs/.agents}/knowledge-map.md +0 -0
  29. {coding_proxy-0.5.1a2/docs/agents → coding_proxy-0.5.1a4/docs/.agents}/reference-specifications.md +0 -0
  30. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/arch/config-reference.md +0 -0
  31. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/arch/convert.md +0 -0
  32. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/arch/design-patterns.md +0 -0
  33. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/arch/routing.md +0 -0
  34. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/arch/testing.md +0 -0
  35. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/arch/vendors.md +0 -0
  36. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/framework.md +0 -0
  37. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/guide/api-reference.md +0 -0
  38. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/guide/cli-reference.md +0 -0
  39. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/guide/dashboard.md +0 -0
  40. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/guide/monitoring.md +0 -0
  41. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/guide/quickstart.md +0 -0
  42. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/guide/vendors.md +0 -0
  43. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/ops/ci-cd.md +0 -0
  44. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/user-guide.md +0 -0
  45. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/docs/zh-CN/README.md +0 -0
  46. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/__init__.py +0 -0
  47. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/__init__.py +0 -0
  48. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/__main__.py +0 -0
  49. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/__init__.py +0 -0
  50. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/providers/__init__.py +0 -0
  51. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/providers/base.py +0 -0
  52. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/providers/github.py +0 -0
  53. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/providers/google.py +0 -0
  54. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/runtime.py +0 -0
  55. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/auth/store.py +0 -0
  56. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/cli/__init__.py +0 -0
  57. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/cli/auth_commands.py +0 -0
  58. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/cli/banner.py +0 -0
  59. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/compat/__init__.py +0 -0
  60. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/compat/canonical.py +0 -0
  61. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/compat/session_store.py +0 -0
  62. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/__init__.py +0 -0
  63. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/auth_schema.py +0 -0
  64. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/loader.py +0 -0
  65. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/resiliency.py +0 -0
  66. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/routing.py +0 -0
  67. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/schema.py +0 -0
  68. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/server.py +0 -0
  69. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/session_policy.py +0 -0
  70. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/config/vendors.py +0 -0
  71. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/__init__.py +0 -0
  72. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  73. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  74. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  75. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  76. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  77. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/logging/__init__.py +0 -0
  78. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/logging/db.py +0 -0
  79. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/logging/formatters.py +0 -0
  80. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/logging/stats.py +0 -0
  81. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/__init__.py +0 -0
  82. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/auth.py +0 -0
  83. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/compat.py +0 -0
  84. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/constants.py +0 -0
  85. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/pricing.py +0 -0
  86. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/token.py +0 -0
  87. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/model/vendor.py +0 -0
  88. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/__init__.py +0 -0
  89. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/config.py +0 -0
  90. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  91. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  92. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  93. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  94. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/handler.py +0 -0
  95. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/operation.py +0 -0
  96. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/routes.py +0 -0
  97. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/native_api/usage_registry.py +0 -0
  98. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/pricing.py +0 -0
  99. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/__init__.py +0 -0
  100. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  101. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/error_classifier.py +0 -0
  102. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/model_mapper.py +0 -0
  103. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/quota_guard.py +0 -0
  104. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/rate_limit.py +0 -0
  105. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/retry.py +0 -0
  106. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/router.py +0 -0
  107. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/session_manager.py +0 -0
  108. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/session_policy.py +0 -0
  109. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/tier.py +0 -0
  110. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/usage_parser.py +0 -0
  111. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/routing/usage_recorder.py +0 -0
  112. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/server/__init__.py +0 -0
  113. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/server/app.py +0 -0
  114. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/server/factory.py +0 -0
  115. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/server/responses.py +0 -0
  116. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/streaming/__init__.py +0 -0
  117. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  118. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/__init__.py +0 -0
  119. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/alibaba.py +0 -0
  120. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/anthropic.py +0 -0
  121. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/antigravity.py +0 -0
  122. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/base.py +0 -0
  123. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/concurrency.py +0 -0
  124. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/copilot.py +0 -0
  125. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/copilot_models.py +0 -0
  126. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  127. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  128. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/doubao.py +0 -0
  129. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/kimi.py +0 -0
  130. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/minimax.py +0 -0
  131. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/mixins.py +0 -0
  132. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  133. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/token_manager.py +0 -0
  134. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/xiaomi.py +0 -0
  135. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/src/coding/proxy/vendors/zhipu.py +0 -0
  136. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/__init__.py +0 -0
  137. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/e2e/__init__.py +0 -0
  138. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/e2e/conftest.py +0 -0
  139. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/e2e/test_e2e_token.py +0 -0
  140. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/e2e/test_e2e_vendor.py +0 -0
  141. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_antigravity.py +0 -0
  142. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_auto_login.py +0 -0
  143. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_banner.py +0 -0
  144. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_circuit_breaker.py +0 -0
  145. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_cli_usage.py +0 -0
  146. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_compat.py +0 -0
  147. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_concurrency_monitor.py +0 -0
  148. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_config_init.py +0 -0
  149. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_config_loader.py +0 -0
  150. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_convert_request.py +0 -0
  151. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_convert_response.py +0 -0
  152. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_convert_sse.py +0 -0
  153. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_copilot.py +0 -0
  154. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_copilot_convert_request.py +0 -0
  155. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_copilot_convert_response.py +0 -0
  156. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_copilot_models.py +0 -0
  157. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_copilot_urls.py +0 -0
  158. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_currency.py +0 -0
  159. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_error_classifier.py +0 -0
  160. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_executor_in_flight_tracking.py +0 -0
  161. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_logging_dual_write.py +0 -0
  162. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_mixins.py +0 -0
  163. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_auth.py +0 -0
  164. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_compat.py +0 -0
  165. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_constants.py +0 -0
  166. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_mapper.py +0 -0
  167. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_pricing.py +0 -0
  168. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_token.py +0 -0
  169. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_model_vendor.py +0 -0
  170. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_native_api_base_url_override.py +0 -0
  171. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_native_api_extractors.py +0 -0
  172. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_native_api_handler.py +0 -0
  173. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_native_api_operation.py +0 -0
  174. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_native_api_routes.py +0 -0
  175. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_native_vendors.py +0 -0
  176. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_parse_usage.py +0 -0
  177. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_parse_usage_gemini.py +0 -0
  178. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_pricing.py +0 -0
  179. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_quota_guard.py +0 -0
  180. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_rate_limit.py +0 -0
  181. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_router_chain.py +0 -0
  182. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_runtime_reauth.py +0 -0
  183. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_schema.py +0 -0
  184. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_session_aware.py +0 -0
  185. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_streaming_anthropic_compat.py +0 -0
  186. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_tier.py +0 -0
  187. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_tiers_config.py +0 -0
  188. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_time_range.py +0 -0
  189. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_token_logger.py +0 -0
  190. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_token_logger_native_columns.py +0 -0
  191. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_token_manager.py +0 -0
  192. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_types.py +0 -0
  193. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_vendor_streaming.py +0 -0
  194. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_vendors.py +0 -0
  195. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_zhipu.py +0 -0
  196. {coding_proxy-0.5.1a2 → coding_proxy-0.5.1a4}/tests/test_zhipu_concurrency.py +0 -0
@@ -39,15 +39,15 @@
39
39
  3. **Link Validity**: 确保所有引用的 URL 可访问且具备明确的上下文价值;
40
40
  4. **Testing**: 统一在 tests/ 下维护测试用例,区分单元测试(unit)和集成测试(integration),所有测试的本地运行总时间控制在 3 min 以内;
41
41
  5. **Pre-commit Hooks**: 首次克隆仓库使用 `uv run pre-commit install` 激活本地 Git hooks,使 Ruff lint(含 auto-fix)、Ruff format 及通用代码卫生检查在每次 commit 前自动运行。若 hooks 自动修复了问题,提交会被中断,执行 `git add -p` 审阅修复内容后重新提交即可;
42
- 6. **Issue**: 在 [issue.md](docs/agents/issue.md) 中维护你处理过的 Issue 摘要(问题描述、表因根因、处理方式、后续防范、同类问题影响与处理注意事项等),便于同类问题的跨上下文处理;注意识别相同 Issue,不要同 Issue 多处维护;
42
+ 6. **Issue**: 在 [issue.md](docs/.agents/issue.md) 中维护你处理过的 Issue 摘要(问题描述、表因根因、处理方式、后续防范、同类问题影响与处理注意事项等),便于同类问题的跨上下文处理;注意识别相同 Issue,不要同 Issue 多处维护;
43
43
  - **Package Management Standardization (包管理规范)**:
44
44
  1. **Python**: 严禁使用 pip/poetry,**必须**统一使用 `uv` 进行包管理与脚本执行(如 `uv run`);
45
45
  2. **JavaScript/TypeScript**: 严禁使用 npm/yarn,**必须**统一使用 `pnpm` 进行包管理与脚本执行;
46
46
  - **Database Management**: 谨慎操作,数据迁移、测试等操作严禁将现有数据删除,谨慎操作数据迁移的回滚,防止数据被清理。
47
47
  - **In-depth and close to the facts**:系统且全面地进行问题的分析,深入贴近事实,如有疑问,需先发问,不要乱做决定。
48
- - **Browser Validation Protocol (浏览器验证准则)**:Agent 不得自行完成、绕过或模拟任何 OAuth / SSO 认证流程,所有登录态均来源于用户已认证的 Chrome 主 profile(真实用户登录态)。完整协议(连通性自检、凭证管理、E2E 集成、实机回归等)详见 [浏览器验证协议](./docs/agents/browser-validation.md);
48
+ - **Browser Validation Protocol (浏览器验证准则)**:Agent 不得自行完成、绕过或模拟任何 OAuth / SSO 认证流程,所有登录态均来源于用户已认证的 Chrome 主 profile(真实用户登录态)。完整协议(连通性自检、凭证管理、E2E 集成、实机回归等)详见 [浏览器验证协议](./docs/.agents/browser-validation.md);
49
49
  1. **安全红线**:禁止在 Sandbox 浏览器中跳转 Google 同意屏;禁止以模拟用户或第三方账号替代真实登录态;禁止要求用户在 chat 中粘贴密码、Cookie 或验证码;
50
- - **Knowledge Map (知识索引)**:项目所有文档索引统一维护在 [知识索引](./docs/agents/knowledge-map.md),并在文档目录变更时即时同步跟新;
50
+ - **Knowledge Map (知识索引)**:项目所有文档索引统一维护在 [知识索引](./docs/.agents/knowledge-map.md),并在文档目录变更时即时同步跟新;
51
51
  - **Documentation Standards (文档规范)**:
52
52
  1. **Visual Documentation (图文并茂)**: 对于复杂逻辑,优先 **Mermaid Visualization Norms (Mermaid 可视化规范)**,构建”图文并茂”的直观文档;
53
53
  - **色彩语义与兼容性**:为图表节点配置具备语义辨识度的色彩,并确保在深色模式(Dark Mode)下具有极高的对比度与清晰度;
@@ -55,4 +55,4 @@
55
55
  2. **语言叙事**:用语精准,叙事完备,行文专业,聚焦核心,篇幅精炼,形象具体,体现真实作用与用户吸引性,字数恰当;
56
56
  3. **Direct Hyperlinking (直接跳转)**: 在文档中提及 Repo 内其他资源(文档/代码)时,**必须**构建可跳转的相对路径链接(如 `[Doc Name](./path.md)`),严禁使用”死文本”引用,以降低信息检索熵;
57
57
  4. **实操截图**:文档需要引入必要的浏览器实操截图时,需自行通过默认浏览器打开相关页面,通过实操现场截图并保留到文档路径进行文档引用;
58
- - **Reference Specifications (IEEE)**:为保障工程决策的可追溯性与学术严谨性,核心引用需遵循 [reference-specifications.md](docs/agents/reference-specifications.md)IEEE 标准引用格式;
58
+ - **Reference Specifications (IEEE)**:为保障工程决策的可追溯性与学术严谨性,核心引用需遵循 [reference-specifications.md](docs/.agents/reference-specifications.md)IEEE 标准引用格式;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.5.1a2
3
+ Version: 0.5.1a4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.5.1a2"
3
+ version = "0.5.1a4"
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"
@@ -339,6 +339,12 @@ model_mapping:
339
339
  # 未配置定价的模型在统计中 Cost 列显示 "-"
340
340
  pricing:
341
341
  # ── Anthropic Claude ──
342
+ - vendor: anthropic
343
+ model: claude-opus-4-8
344
+ input_cost_per_mtok: $5.0
345
+ output_cost_per_mtok: $25.0
346
+ cache_write_cost_per_mtok: $6.25
347
+ cache_read_cost_per_mtok: $0.50
342
348
  - vendor: anthropic
343
349
  model: claude-opus-4-7
344
350
  input_cost_per_mtok: $5.0
@@ -52,6 +52,117 @@ def get_transition_channel(
52
52
  # ── 共享辅助函数 ──────────────────────────────────────────────
53
53
 
54
54
 
55
+ def _dump_message_digest(
56
+ messages: list[dict[str, Any]],
57
+ *,
58
+ max_messages: int = 10,
59
+ label: str = "",
60
+ ) -> None:
61
+ """输出前 N 条消息的结构摘要(role + content_type_counts),用于过渡管线诊断.
62
+
63
+ 仅在 DEBUG 级别输出,且仅在消息数 > 0 时才输出,避免噪声。
64
+ """
65
+ if not messages or not logger.isEnabledFor(logging.DEBUG):
66
+ return
67
+ parts: list[str] = [f"[{label}]" if label else ""]
68
+ limit = min(len(messages), max_messages)
69
+ for idx in range(limit):
70
+ msg = messages[idx]
71
+ role = msg.get("role", "?") if isinstance(msg, dict) else "?"
72
+ content = msg.get("content") if isinstance(msg, dict) else None
73
+ if isinstance(content, list):
74
+ type_counts: dict[str, int] = {}
75
+ for b in content:
76
+ if isinstance(b, dict):
77
+ t = b.get("type", "?")
78
+ type_counts[t] = type_counts.get(t, 0) + 1
79
+ else:
80
+ type_counts["raw"] = type_counts.get("raw", 0) + 1
81
+ counts_str = ",".join(f"{t}:{c}" for t, c in sorted(type_counts.items()))
82
+ elif isinstance(content, str):
83
+ counts_str = f"str({len(content)})"
84
+ else:
85
+ counts_str = "empty"
86
+ parts.append(f"{idx}:{role}[{counts_str}]")
87
+ if len(messages) > max_messages:
88
+ parts.append(f"...+{len(messages) - max_messages}more")
89
+ logger.debug("Transition digest %s", " ".join(parts))
90
+
91
+
92
+ def _validate_anthropic_pairing(
93
+ messages: list[dict[str, Any]],
94
+ *,
95
+ context: str = "",
96
+ ) -> list[str]:
97
+ """独立的 Anthropic tool_use/tool_result 配对自检(过渡管线末端执行).
98
+
99
+ 与 ``_enforce_pairing_sanity_pass`` 不同,此函数:
100
+ - 不修改消息列表(纯检测)
101
+ - 针对每个 assistant + tool_use,精确记录下一条 user 消息中匹配/缺失的 ID
102
+ - 发现不一致时输出 WARNING 级别日志含 message index 与具体 ID
103
+
104
+ Returns:
105
+ 检测到的问题描述列表(空列表表示全部通过)。
106
+ """
107
+ issues: list[str] = []
108
+ for i, msg in enumerate(messages):
109
+ if not isinstance(msg, dict) or msg.get("role") != "assistant":
110
+ continue
111
+ content = msg.get("content")
112
+ if not isinstance(content, list):
113
+ continue
114
+ tool_use_ids = [
115
+ b["id"]
116
+ for b in content
117
+ if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("id")
118
+ ]
119
+ if not tool_use_ids:
120
+ continue
121
+
122
+ next_idx = i + 1
123
+ if next_idx >= len(messages):
124
+ issues.append(f"messages[{i}]: assistant with tool_uses at end of list")
125
+ continue
126
+
127
+ next_msg = messages[next_idx]
128
+ if not isinstance(next_msg, dict) or next_msg.get("role") != "user":
129
+ issues.append(
130
+ f"messages[{i}]: next messages[{next_idx}] is not user "
131
+ f"(role={next_msg.get('role') if isinstance(next_msg, dict) else '?'})"
132
+ )
133
+ continue
134
+
135
+ user_content = next_msg.get("content")
136
+ if not isinstance(user_content, list):
137
+ user_content = []
138
+
139
+ result_ids = {
140
+ b["tool_use_id"]
141
+ for b in user_content
142
+ if isinstance(b, dict)
143
+ and b.get("type") == "tool_result"
144
+ and isinstance(b.get("tool_use_id"), str)
145
+ }
146
+
147
+ missing = [uid for uid in tool_use_ids if uid not in result_ids]
148
+ if missing:
149
+ issue = (
150
+ f"messages[{i}]: {len(missing)}/{len(tool_use_ids)} tool_use(s) "
151
+ f"without tool_result in messages[{next_idx}]: {missing[:5]}"
152
+ )
153
+ issues.append(issue)
154
+
155
+ if issues:
156
+ prefix = f"[{context}] " if context else ""
157
+ logger.warning(
158
+ "Anthropic pairing validation: %s%d issue(s) found: %s",
159
+ prefix,
160
+ len(issues),
161
+ "; ".join(issues),
162
+ )
163
+ return issues
164
+
165
+
55
166
  def strip_thinking_blocks(body: dict[str, Any]) -> int:
56
167
  """从 assistant 消息中移除 thinking/redacted_thinking 块(就地).
57
168
 
@@ -678,6 +789,7 @@ def prepare_zhipu_to_anthropic(
678
789
  2. 改写 ``srvtoolu_*`` ID 与 ``server_tool_use`` 类型为标准 Anthropic 形式
679
790
  3. 强制 tool_use/tool_result 配对(单遍正向扫描)
680
791
  4. 剥离 thinking blocks(signature 无效)
792
+ 5. 独立的 Anthropic 兼容性自检(纯检测,不修改,定位 enforce/sanity 未覆盖的边界 case)
681
793
 
682
794
  所有变换均为幂等操作,安全地在已清理的请求体上重复执行。
683
795
 
@@ -686,6 +798,10 @@ def prepare_zhipu_to_anthropic(
686
798
  """
687
799
  prepared = copy.deepcopy(body)
688
800
  adaptations: list[str] = []
801
+ msgs = prepared.get("messages", [])
802
+
803
+ # ── 过渡管线诊断:变换前快照 ──
804
+ _dump_message_digest(msgs, label="zhipu→anthropic.before")
689
805
 
690
806
  # Step 1: 剥离 zhipu 私有 content block 类型(如 server_tool_use_delta)
691
807
  removed_vendor_blocks = _remove_vendor_blocks(prepared, _ZHIPU_VENDOR_BLOCK_TYPES)
@@ -696,16 +812,24 @@ def prepare_zhipu_to_anthropic(
696
812
  rewritten, _ = _rewrite_srvtoolu_ids(prepared)
697
813
  if rewritten:
698
814
  adaptations.append(f"rewritten_{rewritten}_srvtoolu_ids")
815
+ _dump_message_digest(msgs, label="zhipu→anthropic.after_rewrite")
699
816
 
700
817
  # Step 3: 强制 tool_use/tool_result 配对
701
- pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
818
+ pairing_fixes = enforce_anthropic_tool_pairing(msgs)
702
819
  if pairing_fixes:
703
820
  adaptations.extend(pairing_fixes)
821
+ _dump_message_digest(msgs, label="zhipu→anthropic.after_enforce")
704
822
 
705
823
  # Step 4: 剥离 thinking blocks(zhipu signature 无效)
706
824
  stripped = strip_thinking_blocks(prepared)
707
825
  if stripped:
708
826
  adaptations.append(f"stripped_{stripped}_thinking_blocks")
827
+ _dump_message_digest(msgs, label="zhipu→anthropic.after_strip")
828
+
829
+ # Step 5: 独立的 Anthropic 兼容性自检(纯检测,不修改)
830
+ validation_issues = _validate_anthropic_pairing(msgs, context="zhipu→anthropic")
831
+ if validation_issues:
832
+ adaptations.append("anthropic_pairing_validation_issues")
709
833
 
710
834
  return prepared, adaptations
711
835
 
@@ -54,7 +54,7 @@ from ..model.compat import CanonicalRequest
54
54
 
55
55
  logger = logging.getLogger(__name__)
56
56
 
57
- _SESSION_TITLE_MAX_LEN = 30
57
+ _SESSION_TITLE_MAX_LEN = 600
58
58
 
59
59
  # Claude Code 注入的"噪声"标签 — 系统级上下文,不应进入 Session 标题。
60
60
  # 这些标签由 CC harness 在首个 user 消息 content 中拼接,高度同质,
@@ -63,7 +63,7 @@ _NOISE_TAG_PATTERN = re.compile(
63
63
  r"<(?P<tag>system-reminder|user-preferences|"
64
64
  r"local-command-stdout|local-command-stderr|"
65
65
  r"bash-input|bash-stdout|bash-stderr|"
66
- r"ide_selection|stdin|system_instruction)\b[^>]*>"
66
+ r"ide_selection|stdin|system_instruction|session)\b[^>]*>"
67
67
  r".*?</(?P=tag)>",
68
68
  flags=re.DOTALL | re.IGNORECASE,
69
69
  )
@@ -444,6 +444,21 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
444
444
  .detail-card .detail-item { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
445
445
  .detail-card .detail-label { font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .3px; }
446
446
  .detail-card .detail-value { color: var(--text-primary); line-height: 1.4; word-break: break-all; overflow-wrap: break-word; }
447
+ .detail-title-row {
448
+ padding-bottom: 10px; margin-bottom: 10px;
449
+ border-bottom: 1px solid var(--border);
450
+ }
451
+ .detail-title-row .detail-value {
452
+ white-space: normal;
453
+ word-break: break-word;
454
+ overflow-wrap: break-word;
455
+ line-height: 1.5;
456
+ max-height: 4.5em;
457
+ overflow: hidden;
458
+ display: -webkit-box;
459
+ -webkit-line-clamp: 3;
460
+ -webkit-box-orient: vertical;
461
+ }
447
462
  .detail-identity-row {
448
463
  display: flex; gap: 16px;
449
464
  padding-bottom: 10px; margin-bottom: 10px;
@@ -1350,7 +1365,7 @@ function startModelCallingPoll() {
1350
1365
  }).catch(function() {});
1351
1366
  }
1352
1367
  tick();
1353
- _mcTimer = setInterval(tick, 1500);
1368
+ _mcTimer = setInterval(tick, 10000);
1354
1369
  }
1355
1370
  function stopModelCallingPoll() {
1356
1371
  if (_mcTimer) { clearInterval(_mcTimer); _mcTimer = null; }
@@ -1903,9 +1918,11 @@ function renderSessionPage() {
1903
1918
  '<td>' + formatCategories(s.client_categories) + '</td>' +
1904
1919
  '</tr>' +
1905
1920
  '<tr class="row-detail"><td colspan="11"><div class="detail-card">' +
1921
+ '<div class="detail-title-row">' +
1922
+ '<div class="detail-item"><div class="detail-label">Title</div><div class="detail-value">' + (sessionTitle ? escapeHtml(sessionTitle) : '–') + '</div></div>' +
1923
+ '</div>' +
1906
1924
  '<div class="detail-identity-row">' +
1907
1925
  '<div class="detail-item"><div class="detail-label">Session ID</div><div class="detail-value" title="' + escapeHtml(s.session_key) + '">' + escapeHtml(parsed.session_id || s.session_key) + '</div></div>' +
1908
- '<div class="detail-item"><div class="detail-label">Title</div><div class="detail-value">' + (sessionTitle ? escapeHtml(sessionTitle) : '–') + '</div></div>' +
1909
1926
  '<div class="detail-item"><div class="detail-label">Device</div><div class="detail-value" title="' + escapeHtml(parsed.device_id || '') + '">' + (parsed.device_id ? escapeHtml(parsed.device_id) : '–') + '</div></div>' +
1910
1927
  '<div class="detail-item"><div class="detail-label">Account</div><div class="detail-value" title="' + escapeHtml(parsed.account_uuid || '') + '">' + (parsed.account_uuid ? escapeHtml(parsed.account_uuid) : '–') + '</div></div>' +
1911
1928
  '</div>' +
@@ -8,7 +8,7 @@ from typing import Any
8
8
 
9
9
  import httpx
10
10
  from fastapi import Request, Response
11
- from fastapi.responses import StreamingResponse
11
+ from fastapi.responses import RedirectResponse, StreamingResponse
12
12
 
13
13
  from ..vendors.base import NoCompatibleVendorError
14
14
 
@@ -197,11 +197,15 @@ def register_health_routes(app: Any) -> None:
197
197
  return {"status": "ok"}
198
198
 
199
199
  @app.head("/")
200
- @app.get("/")
201
- async def root() -> Response:
200
+ async def root_head() -> Response:
202
201
  """根路径连通性探测 — Claude Code 在建连前发送 HEAD / 作为 health probe."""
203
202
  return Response(status_code=200)
204
203
 
204
+ @app.get("/")
205
+ async def root_get() -> RedirectResponse:
206
+ """GET / 重定向到 Dashboard."""
207
+ return RedirectResponse(url="/dashboard", status_code=307)
208
+
205
209
 
206
210
  def register_status_route(app: Any, router: Any) -> None:
207
211
  """注册状态查询路由."""
@@ -188,13 +188,16 @@ async def test_http_health_probe(e2e_client: object) -> None:
188
188
  )
189
189
 
190
190
  get_resp = await e2e_client.get("/")
191
- assert get_resp.status_code == 200, f"GET / 预期 200,实际 {get_resp.status_code}"
191
+ assert get_resp.status_code == 307, f"GET / 预期 307,实际 {get_resp.status_code}"
192
+ assert get_resp.headers["location"] == "/dashboard"
192
193
 
193
194
  health_resp = await e2e_client.get("/health")
194
195
  assert health_resp.status_code == 200
195
196
  assert health_resp.json() == {"status": "ok"}
196
197
 
197
- print("\n[E2E] HTTP health probe 成功: HEAD /=200, GET /=200, /health=ok")
198
+ print(
199
+ "\n[E2E] HTTP health probe 成功: HEAD /=200, GET /=307→/dashboard, /health=ok"
200
+ )
198
201
 
199
202
 
200
203
  @pytest.mark.e2e
@@ -35,11 +35,12 @@ def test_head_root_returns_200():
35
35
  assert resp.status_code == 200
36
36
 
37
37
 
38
- def test_get_root_returns_200():
39
- """GET / 返回 200."""
38
+ def test_get_root_redirects_to_dashboard():
39
+ """GET / 重定向到 /dashboard."""
40
40
  with _make_app() as client:
41
- resp = client.get("/")
42
- assert resp.status_code == 200
41
+ resp = client.get("/", follow_redirects=False)
42
+ assert resp.status_code == 307
43
+ assert resp.headers["location"] == "/dashboard"
43
44
 
44
45
 
45
46
  # ── count_tokens 透传 ────────────────────────────────────────
@@ -2249,6 +2249,15 @@ class TestSanitizeUserText:
2249
2249
  raw = "<local-command-stdout>build ok</local-command-stdout>构建后的下一步问题"
2250
2250
  assert _sanitize_user_text(raw) == "构建后的下一步问题"
2251
2251
 
2252
+ def test_strips_session_tag(self):
2253
+ """``<session>`` 标签应被完整剥离,不残留在标题中."""
2254
+ raw = "<session>session metadata</session>用户真实输入文本"
2255
+ assert _sanitize_user_text(raw) == "用户真实输入文本"
2256
+
2257
+ def test_strips_session_tag_multiline(self):
2258
+ raw = "<session>\nline1\nline2\n</session>真实标题"
2259
+ assert _sanitize_user_text(raw) == "真实标题"
2260
+
2252
2261
 
2253
2262
  class TestExtractSessionTitle:
2254
2263
  """``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题."""
@@ -2258,7 +2267,7 @@ class TestExtractSessionTitle:
2258
2267
  return build_canonical_request({"model": "test", "messages": messages}, {})
2259
2268
 
2260
2269
  def test_truncates_to_max_len(self):
2261
- long_text = "用户输入文本" * 20
2270
+ long_text = "用户输入文本" * 200
2262
2271
  req = self._build_request([{"role": "user", "content": long_text}])
2263
2272
  title = _extract_session_title(req)
2264
2273
  assert len(title) == _SESSION_TITLE_MAX_LEN
@@ -2225,3 +2225,221 @@ class TestNormalizeForZhipu:
2225
2225
  assert result["stream"] is True
2226
2226
  assert result["metadata"] == {"user_id": "test"}
2227
2227
  assert adaptations == []
2228
+
2229
+
2230
+ class TestDumpMessageDigest:
2231
+ """``_dump_message_digest`` 诊断快照函数测试."""
2232
+
2233
+ def test_outputs_nothing_on_empty_messages(self, caplog):
2234
+ import logging
2235
+
2236
+ from coding.proxy.convert.vendor_channels import _dump_message_digest
2237
+
2238
+ with caplog.at_level(
2239
+ logging.DEBUG, logger="coding.proxy.convert.vendor_channels"
2240
+ ):
2241
+ _dump_message_digest([], label="test")
2242
+ assert "Transition digest" not in caplog.text
2243
+
2244
+ def test_outputs_structure_for_first_n_messages(self, caplog):
2245
+ import logging
2246
+
2247
+ from coding.proxy.convert.vendor_channels import _dump_message_digest
2248
+
2249
+ messages = [
2250
+ {"role": "user", "content": [{"type": "text", "text": "hi"}]},
2251
+ {
2252
+ "role": "assistant",
2253
+ "content": [
2254
+ {"type": "tool_use", "id": "toolu_1", "name": "bash", "input": {}},
2255
+ ],
2256
+ },
2257
+ {
2258
+ "role": "user",
2259
+ "content": [
2260
+ {"type": "tool_result", "tool_use_id": "toolu_1", "content": "ok"},
2261
+ ],
2262
+ },
2263
+ ]
2264
+ with caplog.at_level(
2265
+ logging.DEBUG, logger="coding.proxy.convert.vendor_channels"
2266
+ ):
2267
+ _dump_message_digest(messages, label="test")
2268
+ assert "test" in caplog.text
2269
+ assert "0:user" in caplog.text
2270
+ assert "1:assistant" in caplog.text
2271
+ assert "tool_use:1" in caplog.text
2272
+
2273
+
2274
+ class TestValidateAnthropicPairing:
2275
+ """``_validate_anthropic_pairing`` 独立配对自检测试."""
2276
+
2277
+ def test_no_issues_for_correct_pairing(self):
2278
+ from coding.proxy.convert.vendor_channels import _validate_anthropic_pairing
2279
+
2280
+ messages = [
2281
+ {"role": "user", "content": "go"},
2282
+ {
2283
+ "role": "assistant",
2284
+ "content": [
2285
+ {"type": "tool_use", "id": "toolu_1", "name": "bash", "input": {}},
2286
+ ],
2287
+ },
2288
+ {
2289
+ "role": "user",
2290
+ "content": [
2291
+ {"type": "tool_result", "tool_use_id": "toolu_1", "content": "ok"},
2292
+ ],
2293
+ },
2294
+ ]
2295
+ issues = _validate_anthropic_pairing(messages)
2296
+ assert issues == []
2297
+
2298
+ def test_detects_missing_tool_result(self):
2299
+ from coding.proxy.convert.vendor_channels import _validate_anthropic_pairing
2300
+
2301
+ messages = [
2302
+ {"role": "user", "content": "go"},
2303
+ {
2304
+ "role": "assistant",
2305
+ "content": [
2306
+ {"type": "tool_use", "id": "toolu_1", "name": "bash", "input": {}},
2307
+ ],
2308
+ },
2309
+ {"role": "user", "content": [{"type": "text", "text": "no result"}]},
2310
+ ]
2311
+ issues = _validate_anthropic_pairing(messages)
2312
+ assert len(issues) == 1
2313
+ assert "toolu_1" in issues[0]
2314
+ assert "messages[1]" in issues[0]
2315
+
2316
+ def test_detects_non_user_after_assistant_with_tool_use(self):
2317
+ from coding.proxy.convert.vendor_channels import _validate_anthropic_pairing
2318
+
2319
+ messages = [
2320
+ {"role": "user", "content": "go"},
2321
+ {
2322
+ "role": "assistant",
2323
+ "content": [
2324
+ {"type": "tool_use", "id": "toolu_1", "name": "bash", "input": {}},
2325
+ ],
2326
+ },
2327
+ {
2328
+ "role": "assistant",
2329
+ "content": [{"type": "text", "text": "another assistant"}],
2330
+ },
2331
+ ]
2332
+ issues = _validate_anthropic_pairing(messages)
2333
+ assert len(issues) == 1
2334
+ assert "not user" in issues[0]
2335
+
2336
+ def test_detects_assistant_with_tool_use_at_end_of_list(self):
2337
+ from coding.proxy.convert.vendor_channels import _validate_anthropic_pairing
2338
+
2339
+ messages = [
2340
+ {"role": "user", "content": "go"},
2341
+ {
2342
+ "role": "assistant",
2343
+ "content": [
2344
+ {"type": "tool_use", "id": "toolu_1", "name": "bash", "input": {}},
2345
+ ],
2346
+ },
2347
+ ]
2348
+ issues = _validate_anthropic_pairing(messages)
2349
+ assert len(issues) == 1
2350
+ assert "end of list" in issues[0]
2351
+
2352
+ def test_partial_missing_only_reports_missing_ids(self):
2353
+ from coding.proxy.convert.vendor_channels import _validate_anthropic_pairing
2354
+
2355
+ messages = [
2356
+ {"role": "user", "content": "go"},
2357
+ {
2358
+ "role": "assistant",
2359
+ "content": [
2360
+ {"type": "tool_use", "id": "toolu_1", "name": "bash", "input": {}},
2361
+ {"type": "tool_use", "id": "toolu_2", "name": "read", "input": {}},
2362
+ ],
2363
+ },
2364
+ {
2365
+ "role": "user",
2366
+ "content": [
2367
+ {"type": "tool_result", "tool_use_id": "toolu_1", "content": "ok"},
2368
+ ],
2369
+ },
2370
+ ]
2371
+ issues = _validate_anthropic_pairing(messages)
2372
+ assert len(issues) == 1
2373
+ assert "toolu_2" in issues[0]
2374
+ assert "1/2" in issues[0]
2375
+
2376
+ def test_integration_with_zhipu_to_anthropic_channel(self):
2377
+ """验证 prepare_zhipu_to_anthropic 在末端执行自检且 adaptations 包含标签."""
2378
+ from coding.proxy.convert.vendor_channels import prepare_zhipu_to_anthropic
2379
+
2380
+ body = {
2381
+ "model": "claude-opus-4-7",
2382
+ "messages": [
2383
+ {"role": "user", "content": "go"},
2384
+ {
2385
+ "role": "assistant",
2386
+ "content": [
2387
+ {
2388
+ "type": "server_tool_use",
2389
+ "id": "srvtoolu_01",
2390
+ "name": "bash",
2391
+ "input": {},
2392
+ },
2393
+ ],
2394
+ },
2395
+ {
2396
+ "role": "user",
2397
+ "content": [
2398
+ {
2399
+ "type": "tool_result",
2400
+ "tool_use_id": "srvtoolu_01",
2401
+ "content": "ok",
2402
+ },
2403
+ ],
2404
+ },
2405
+ ],
2406
+ }
2407
+ result, adaptations = prepare_zhipu_to_anthropic(body)
2408
+ # 自检通过,不应包含 validation_issues 标签
2409
+ assert "anthropic_pairing_validation_issues" not in adaptations
2410
+
2411
+ def test_integration_detects_enforce_missed_issue(self):
2412
+ """构造一个理论上 enforce 可能遗漏的场景,验证自检能捕获.
2413
+
2414
+ 场景:两条连续 assistant 消息,第一条的 tool_result 被第二条的
2415
+ existing_result_ids"冒领"(相同 ID 碰撞场景的模拟)。
2416
+ 虽然当前 enforce 实现下不太可能自然产生此场景,但自检应能捕获。
2417
+ """
2418
+ from coding.proxy.convert.vendor_channels import (
2419
+ _validate_anthropic_pairing,
2420
+ )
2421
+
2422
+ # 手动构造一个 enforce 后仍存在配对缺陷的 body
2423
+ messages = [
2424
+ {"role": "user", "content": "go"},
2425
+ {
2426
+ "role": "assistant",
2427
+ "content": [
2428
+ {"type": "tool_use", "id": "toolu_x", "name": "bash", "input": {}},
2429
+ ],
2430
+ },
2431
+ {
2432
+ "role": "user",
2433
+ "content": [
2434
+ # tool_result 缺失 tolu_x,但有不相关的 tool_result
2435
+ {
2436
+ "type": "tool_result",
2437
+ "tool_use_id": "toolu_other",
2438
+ "content": "wrong",
2439
+ },
2440
+ ],
2441
+ },
2442
+ ]
2443
+ issues = _validate_anthropic_pairing(messages)
2444
+ assert len(issues) == 1
2445
+ assert "toolu_x" in issues[0]
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "coding-proxy"
77
- version = "0.5.1a2"
77
+ version = "0.5.1a4"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "aiosqlite" },
File without changes
File without changes
File without changes