coding-proxy 0.5.1a4__tar.gz → 0.5.1a6__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.1a4 → coding_proxy-0.5.1a6}/PKG-INFO +1 -1
  2. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/pyproject.toml +1 -1
  3. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/logging/db.py +14 -0
  4. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/executor.py +111 -12
  5. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/usage_recorder.py +5 -0
  6. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_router_executor.py +322 -4
  7. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/uv.lock +1 -1
  8. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/.github/workflows/ci.yml +0 -0
  9. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/.github/workflows/coverage.yml +0 -0
  10. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/.github/workflows/release.yml +0 -0
  11. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/.gitignore +0 -0
  12. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/.pre-commit-config.yaml +0 -0
  13. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/AGENTS.md +0 -0
  14. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/CHANGELOG.md +0 -0
  15. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/CLAUDE.md +0 -0
  16. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/LICENSE +0 -0
  17. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/README.md +0 -0
  18. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/assets/dashboard-v0.4.0.png +0 -0
  19. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/assets/model-calling-v0.5.0.png +0 -0
  20. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/assets/session-v0.4.0.png +0 -0
  21. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/.agents/browser-validation.md +0 -0
  22. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/.agents/issue.md +0 -0
  23. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/.agents/knowledge-map.md +0 -0
  24. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/.agents/reference-specifications.md +0 -0
  25. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/arch/config-reference.md +0 -0
  26. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/arch/convert.md +0 -0
  27. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/arch/design-patterns.md +0 -0
  28. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/arch/routing.md +0 -0
  29. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/arch/testing.md +0 -0
  30. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/arch/vendors.md +0 -0
  31. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/framework.md +0 -0
  32. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/guide/api-reference.md +0 -0
  33. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/guide/cli-reference.md +0 -0
  34. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/guide/dashboard.md +0 -0
  35. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/guide/monitoring.md +0 -0
  36. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/guide/quickstart.md +0 -0
  37. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/guide/vendors.md +0 -0
  38. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/ops/ci-cd.md +0 -0
  39. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/user-guide.md +0 -0
  40. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/docs/zh-CN/README.md +0 -0
  41. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/__init__.py +0 -0
  42. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/__init__.py +0 -0
  43. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/__main__.py +0 -0
  44. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/__init__.py +0 -0
  45. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/providers/__init__.py +0 -0
  46. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/providers/base.py +0 -0
  47. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/providers/github.py +0 -0
  48. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/providers/google.py +0 -0
  49. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/runtime.py +0 -0
  50. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/auth/store.py +0 -0
  51. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/cli/__init__.py +0 -0
  52. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/cli/auth_commands.py +0 -0
  53. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/cli/banner.py +0 -0
  54. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/compat/__init__.py +0 -0
  55. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/compat/canonical.py +0 -0
  56. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/compat/session_store.py +0 -0
  57. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/__init__.py +0 -0
  58. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/auth_schema.py +0 -0
  59. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/config.default.yaml +0 -0
  60. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/loader.py +0 -0
  61. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/resiliency.py +0 -0
  62. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/routing.py +0 -0
  63. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/schema.py +0 -0
  64. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/server.py +0 -0
  65. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/session_policy.py +0 -0
  66. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/config/vendors.py +0 -0
  67. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/__init__.py +0 -0
  68. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  69. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  70. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  71. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  72. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  73. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/convert/vendor_channels.py +0 -0
  74. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/logging/__init__.py +0 -0
  75. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/logging/formatters.py +0 -0
  76. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/logging/stats.py +0 -0
  77. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/__init__.py +0 -0
  78. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/auth.py +0 -0
  79. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/compat.py +0 -0
  80. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/constants.py +0 -0
  81. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/pricing.py +0 -0
  82. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/token.py +0 -0
  83. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/model/vendor.py +0 -0
  84. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/__init__.py +0 -0
  85. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/config.py +0 -0
  86. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  87. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  88. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  89. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  90. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/handler.py +0 -0
  91. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/operation.py +0 -0
  92. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/routes.py +0 -0
  93. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/native_api/usage_registry.py +0 -0
  94. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/pricing.py +0 -0
  95. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/__init__.py +0 -0
  96. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  97. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/error_classifier.py +0 -0
  98. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/model_mapper.py +0 -0
  99. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/quota_guard.py +0 -0
  100. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/rate_limit.py +0 -0
  101. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/retry.py +0 -0
  102. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/router.py +0 -0
  103. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/session_manager.py +0 -0
  104. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/session_policy.py +0 -0
  105. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/tier.py +0 -0
  106. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/routing/usage_parser.py +0 -0
  107. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/server/__init__.py +0 -0
  108. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/server/app.py +0 -0
  109. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/server/dashboard.py +0 -0
  110. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/server/factory.py +0 -0
  111. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/server/responses.py +0 -0
  112. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/server/routes.py +0 -0
  113. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/streaming/__init__.py +0 -0
  114. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  115. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/__init__.py +0 -0
  116. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/alibaba.py +0 -0
  117. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/anthropic.py +0 -0
  118. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/antigravity.py +0 -0
  119. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/base.py +0 -0
  120. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/concurrency.py +0 -0
  121. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/copilot.py +0 -0
  122. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/copilot_models.py +0 -0
  123. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  124. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  125. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/doubao.py +0 -0
  126. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/kimi.py +0 -0
  127. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/minimax.py +0 -0
  128. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/mixins.py +0 -0
  129. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  130. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/token_manager.py +0 -0
  131. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/xiaomi.py +0 -0
  132. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/src/coding/proxy/vendors/zhipu.py +0 -0
  133. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/__init__.py +0 -0
  134. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/e2e/__init__.py +0 -0
  135. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/e2e/conftest.py +0 -0
  136. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/e2e/test_e2e_http.py +0 -0
  137. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/e2e/test_e2e_token.py +0 -0
  138. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/e2e/test_e2e_vendor.py +0 -0
  139. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_antigravity.py +0 -0
  140. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_app_routes.py +0 -0
  141. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_auto_login.py +0 -0
  142. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_banner.py +0 -0
  143. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_circuit_breaker.py +0 -0
  144. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_cli_usage.py +0 -0
  145. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_compat.py +0 -0
  146. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_concurrency_monitor.py +0 -0
  147. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_config_init.py +0 -0
  148. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_config_loader.py +0 -0
  149. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_convert_request.py +0 -0
  150. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_convert_response.py +0 -0
  151. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_convert_sse.py +0 -0
  152. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_copilot.py +0 -0
  153. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_copilot_convert_request.py +0 -0
  154. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_copilot_convert_response.py +0 -0
  155. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_copilot_models.py +0 -0
  156. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_copilot_urls.py +0 -0
  157. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_currency.py +0 -0
  158. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_error_classifier.py +0 -0
  159. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_executor_in_flight_tracking.py +0 -0
  160. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_logging_dual_write.py +0 -0
  161. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_mixins.py +0 -0
  162. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_auth.py +0 -0
  163. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_compat.py +0 -0
  164. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_constants.py +0 -0
  165. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_mapper.py +0 -0
  166. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_pricing.py +0 -0
  167. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_token.py +0 -0
  168. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_model_vendor.py +0 -0
  169. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_native_api_base_url_override.py +0 -0
  170. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_native_api_extractors.py +0 -0
  171. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_native_api_handler.py +0 -0
  172. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_native_api_operation.py +0 -0
  173. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_native_api_routes.py +0 -0
  174. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_native_vendors.py +0 -0
  175. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_parse_usage.py +0 -0
  176. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_parse_usage_gemini.py +0 -0
  177. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_pricing.py +0 -0
  178. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_quota_guard.py +0 -0
  179. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_rate_limit.py +0 -0
  180. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_router_chain.py +0 -0
  181. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_runtime_reauth.py +0 -0
  182. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_schema.py +0 -0
  183. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_session_aware.py +0 -0
  184. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_streaming_anthropic_compat.py +0 -0
  185. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_tier.py +0 -0
  186. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_tiers_config.py +0 -0
  187. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_time_range.py +0 -0
  188. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_token_logger.py +0 -0
  189. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_token_logger_native_columns.py +0 -0
  190. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_token_manager.py +0 -0
  191. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_types.py +0 -0
  192. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_vendor_channels.py +0 -0
  193. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_vendor_streaming.py +0 -0
  194. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_vendors.py +0 -0
  195. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_zhipu.py +0 -0
  196. {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a6}/tests/test_zhipu_concurrency.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.5.1a4
3
+ Version: 0.5.1a6
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.1a4"
3
+ version = "0.5.1a6"
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"
@@ -335,6 +335,20 @@ class TokenLogger:
335
335
  )
336
336
  await self._db.commit()
337
337
 
338
+ async def update_empty_session_title(self, session_key: str, title: str) -> None:
339
+ """为标题为空的 session 补写标题(幂等,仅覆盖空标题行).
340
+
341
+ 使用 ``AND title = ''`` 条件确保不覆盖已有标题,
342
+ 即使行已存在但标题为空也会被更新。
343
+ """
344
+ if not self._db or not title or not session_key:
345
+ return
346
+ await self._db.execute(
347
+ "UPDATE session_meta SET title = ? WHERE session_key = ? AND title = ''",
348
+ (title, session_key),
349
+ )
350
+ await self._db.commit()
351
+
338
352
  async def get_session_titles(self, session_keys: list[str]) -> dict[str, str]:
339
353
  """批量查询 session 标题."""
340
354
  if not self._db or not session_keys:
@@ -50,11 +50,13 @@ from ..compat.canonical import (
50
50
  CompatibilityStatus,
51
51
  build_canonical_request,
52
52
  )
53
- from ..model.compat import CanonicalRequest
53
+ from ..model.compat import CanonicalMessagePart, CanonicalRequest
54
54
 
55
55
  logger = logging.getLogger(__name__)
56
56
 
57
57
  _SESSION_TITLE_MAX_LEN = 600
58
+ # 回退标题截取长度 — 工具结果等非用户直接输入的摘要上限。
59
+ _FALLBACK_TITLE_MAX_LEN = 80
58
60
 
59
61
  # Claude Code 注入的"噪声"标签 — 系统级上下文,不应进入 Session 标题。
60
62
  # 这些标签由 CC harness 在首个 user 消息 content 中拼接,高度同质,
@@ -63,11 +65,17 @@ _NOISE_TAG_PATTERN = re.compile(
63
65
  r"<(?P<tag>system-reminder|user-preferences|"
64
66
  r"local-command-stdout|local-command-stderr|"
65
67
  r"bash-input|bash-stdout|bash-stderr|"
66
- r"ide_selection|stdin|system_instruction|session)\b[^>]*>"
68
+ r"ide_selection|stdin|system_instruction|session|"
69
+ r"artifactMetadata|thinking)\b[^>]*>"
67
70
  r".*?</(?P=tag)>",
68
71
  flags=re.DOTALL | re.IGNORECASE,
69
72
  )
70
73
 
74
+ # <session> 标签需要特殊处理:当用户文本在 <session> 标签内部时,
75
+ # 完整块剥离会连同用户文本一起删除。此模式仅去除外壳标签(保留内容),
76
+ # 用于首轮完整剥离结果为空时的二次回退提取。
77
+ _SESSION_TAG_WRAPPER = re.compile(r"</?session\b[^>]*>", flags=re.IGNORECASE)
78
+
71
79
  # Slash command 子标签:用于识别 /commit、/review 等命令式调用,
72
80
  # 合成"命令 + 参数"式标题。
73
81
  _CMD_NAME_PATTERN = re.compile(r"<command-name>(.*?)</command-name>", flags=re.DOTALL)
@@ -77,6 +85,9 @@ _CMD_WRAPPER_PATTERN = re.compile(
77
85
  r"<command-[\w-]+>.*?</command-[\w-]+>", flags=re.DOTALL
78
86
  )
79
87
 
88
+ # 空白折叠
89
+ _WHITESPACE_PATTERN = re.compile(r"\s+")
90
+
80
91
 
81
92
  def _sanitize_user_text(raw: str) -> str:
82
93
  """剔除 Claude Code 注入的系统级 XML 块,还原真实用户输入。
@@ -85,8 +96,10 @@ def _sanitize_user_text(raw: str) -> str:
85
96
  1. Slash command 优先识别 — 若检测到 <command-name>,合成"命令 + 参数"
86
97
  式标题(因为残留文本通常为空,直接取标签内容更有意义)。
87
98
  2. 通用噪声剥离 — 移除已知白名单内的 system-reminder 等标签。
88
- 3. 残留 command-* 包裹清除 兜底去除 command-message 等次要标签。
89
- 4. 前后空白归一化 — 折叠连续空白为单空格,便于 30 字截断。
99
+ 3. <session> 二次回退若首轮剥离后为空,说明用户文本可能在 <session>
100
+ 标签内部;此时仅去除外壳标签,保留内部文本再做噪声剥离。
101
+ 4. 残留 command-* 包裹清除 — 兜底去除 command-message 等次要标签。
102
+ 5. 前后空白归一化 — 折叠连续空白为单空格。
90
103
  """
91
104
  if not raw:
92
105
  return ""
@@ -104,18 +117,37 @@ def _sanitize_user_text(raw: str) -> str:
104
117
  # 阶段二: 通用噪声剥离
105
118
  cleaned = _NOISE_TAG_PATTERN.sub("", raw)
106
119
  cleaned = _CMD_WRAPPER_PATTERN.sub("", cleaned)
120
+ cleaned = _WHITESPACE_PATTERN.sub(" ", cleaned).strip()
121
+ if cleaned:
122
+ return cleaned
123
+
124
+ # 阶段三: <session> 二次回退
125
+ # 当首轮全部剥离为空时,用户文本很可能被 <session> 标签完整包裹。
126
+ # 此时不去除 <session> 块,而是仅剥掉外壳标签,保留内部文本后重新剥离。
127
+ if "<session" in raw.lower():
128
+ inner = _SESSION_TAG_WRAPPER.sub("", raw)
129
+ cleaned = _NOISE_TAG_PATTERN.sub("", inner)
130
+ cleaned = _CMD_WRAPPER_PATTERN.sub("", cleaned)
131
+ cleaned = _WHITESPACE_PATTERN.sub(" ", cleaned).strip()
132
+ if cleaned:
133
+ return cleaned
107
134
 
108
- # 阶段三: 空白折叠
109
- return re.sub(r"\s+", " ", cleaned).strip()
135
+ return ""
110
136
 
111
137
 
112
- def _extract_session_title(request: CanonicalRequest) -> str:
113
- """从规范化请求中提取首个用户消息文本作为 session 标题。
138
+ # ── Session 标题提取: 多层级回退策略 ──────────────────────────────
139
+ #
140
+ # Level 1: user TEXT → 噪声剥离 → 首条非空文本 (原有逻辑)
141
+ # Level 2: user TOOL_RESULT → text 截取 → "[Tool output] <snippet>"
142
+ # Level 3: user IMAGE → 计数 → "[1 Image]" / "[N Images]"
143
+ # Level 4: 请求元数据 → tool_names / model → "[Tool call] Bash, Read"
144
+ # / "[Session] claude-opus-4-8"
145
+ # ─────────────────────────────────────────────────────────────────
114
146
 
115
- 跳过 Claude Code 注入的系统级 XML 块(system-reminder、user-preferences 等),
116
- 确保标题反映用户真实输入而非高同质化的系统模板。
117
- """
118
- for part in request.messages:
147
+
148
+ def _extract_title_from_user_text(messages: list[CanonicalMessagePart]) -> str:
149
+ """Level 1: 从 user TEXT 部分提取经噪声剥离后的首条非空文本."""
150
+ for part in messages:
119
151
  if part.role != "user" or part.type != CanonicalPartType.TEXT:
120
152
  continue
121
153
  cleaned = _sanitize_user_text(part.text)
@@ -124,6 +156,59 @@ def _extract_session_title(request: CanonicalRequest) -> str:
124
156
  return ""
125
157
 
126
158
 
159
+ def _extract_title_from_tool_results(messages: list[CanonicalMessagePart]) -> str:
160
+ """Level 2: 从 user TOOL_RESULT 部分截取文本摘要."""
161
+ for part in messages:
162
+ if part.role != "user" or part.type != CanonicalPartType.TOOL_RESULT:
163
+ continue
164
+ if not part.text:
165
+ continue
166
+ cleaned = _sanitize_user_text(part.text)
167
+ if cleaned:
168
+ snippet = cleaned[:_FALLBACK_TITLE_MAX_LEN]
169
+ return f"[Tool output] {snippet}"
170
+ return ""
171
+
172
+
173
+ def _extract_title_from_images(messages: list[CanonicalMessagePart]) -> str:
174
+ """Level 3: 统计 user IMAGE 部分数量,生成图片描述标题."""
175
+ count = sum(
176
+ 1 for p in messages if p.role == "user" and p.type == CanonicalPartType.IMAGE
177
+ )
178
+ if count == 0:
179
+ return ""
180
+ return f"[{count} Image{'s' if count > 1 else ''}]"
181
+
182
+
183
+ def _extract_title_from_metadata(request: CanonicalRequest) -> str:
184
+ """Level 4: 从请求元数据 (tool_names / model) 合成兜底标题."""
185
+ if request.tool_names:
186
+ names = ", ".join(request.tool_names[:3])
187
+ return f"[Tool call] {names}"
188
+ if request.model:
189
+ return f"[Session] {request.model}"
190
+ return ""
191
+
192
+
193
+ def _extract_session_title(request: CanonicalRequest) -> str:
194
+ """从规范化请求中提取 session 标题 — 多层级回退策略。
195
+
196
+ 依次尝试: user TEXT 噪声剥离 → TOOL_RESULT 摘要 → IMAGE 计数 → 元数据兜底。
197
+ 任意级别命中即返回,确保 Dashboard 尽可能展示有辨识度的标题。
198
+ """
199
+ messages = request.messages
200
+ for extractor in (
201
+ _extract_title_from_user_text,
202
+ _extract_title_from_tool_results,
203
+ _extract_title_from_images,
204
+ ):
205
+ title = extractor(messages)
206
+ if title:
207
+ return title[:_SESSION_TITLE_MAX_LEN]
208
+ # Level 4 依赖 request 元数据,签名不同
209
+ return _extract_title_from_metadata(request)[:_SESSION_TITLE_MAX_LEN]
210
+
211
+
127
212
  def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
128
213
  """构建语义拒绝的请求体诊断上下文.
129
214
 
@@ -663,6 +748,13 @@ class _RouteExecutor:
663
748
  await self._recorder.set_session_title(
664
749
  canonical_request.session_key, title
665
750
  )
751
+ else:
752
+ # 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
753
+ title = _extract_session_title(canonical_request)
754
+ if title:
755
+ await self._recorder.update_empty_session_title(
756
+ canonical_request.session_key, title
757
+ )
666
758
  incompatible_reasons: list[str] = []
667
759
  effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
668
760
  last_idx = len(effective_tiers) - 1
@@ -842,6 +934,13 @@ class _RouteExecutor:
842
934
  await self._recorder.set_session_title(
843
935
  canonical_request.session_key, title
844
936
  )
937
+ else:
938
+ # 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
939
+ title = _extract_session_title(canonical_request)
940
+ if title:
941
+ await self._recorder.update_empty_session_title(
942
+ canonical_request.session_key, title
943
+ )
845
944
  incompatible_reasons: list[str] = []
846
945
  effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
847
946
  last_idx = len(effective_tiers) - 1
@@ -33,6 +33,11 @@ class UsageRecorder:
33
33
  if self._token_logger:
34
34
  await self._token_logger.set_session_title(session_key, title)
35
35
 
36
+ async def update_empty_session_title(self, session_key: str, title: str) -> None:
37
+ """为标题为空的 session 补写标题(委托给 TokenLogger)."""
38
+ if self._token_logger:
39
+ await self._token_logger.update_empty_session_title(session_key, title)
40
+
36
41
  # ── 用量信息构建 ──────────────────────────────────────
37
42
 
38
43
  @staticmethod
@@ -20,10 +20,15 @@ from coding.proxy.compat.canonical import (
20
20
  build_canonical_request,
21
21
  )
22
22
  from coding.proxy.routing.executor import (
23
+ _FALLBACK_TITLE_MAX_LEN,
23
24
  _SESSION_TITLE_MAX_LEN,
24
25
  _VENDOR_PROTOCOL_LABEL_MAP,
25
26
  _build_semantic_rejection_diagnostic,
26
27
  _extract_session_title,
28
+ _extract_title_from_images,
29
+ _extract_title_from_metadata,
30
+ _extract_title_from_tool_results,
31
+ _extract_title_from_user_text,
27
32
  _has_tool_results,
28
33
  _is_likely_request_format_error,
29
34
  _log_vendor_response_error,
@@ -2258,6 +2263,54 @@ class TestSanitizeUserText:
2258
2263
  raw = "<session>\nline1\nline2\n</session>真实标题"
2259
2264
  assert _sanitize_user_text(raw) == "真实标题"
2260
2265
 
2266
+ def test_strips_artifact_metadata_tag(self):
2267
+ """``<artifactMetadata>`` 标签应被完整剥离."""
2268
+ raw = "<artifactMetadata>artifact context</artifactMetadata>用户文本"
2269
+ assert _sanitize_user_text(raw) == "用户文本"
2270
+
2271
+ def test_strips_thinking_tag(self):
2272
+ """``<thinking>`` 标签应被完整剥离."""
2273
+ raw = "<thinking>内部推理过程</thinking>用户实际提问"
2274
+ assert _sanitize_user_text(raw) == "用户实际提问"
2275
+
2276
+ def test_strips_thinking_tag_multiline(self):
2277
+ raw = "<thinking>\nline1\nline2\n</thinking>清理后文本"
2278
+ assert _sanitize_user_text(raw) == "清理后文本"
2279
+
2280
+ # ── <session> 标签包裹用户文本的二次回退 ──
2281
+
2282
+ def test_session_tag_wrapping_user_text(self):
2283
+ """当 <session> 标签包裹用户文本时,二次回退应提取内部文本.
2284
+
2285
+ 注: session 元数据可能残留在标题前部,但用户文本现在可见,
2286
+ 远优于完全回退到 '[Session] model_name'.
2287
+ """
2288
+ raw = "<session>session metadata\n用户真实提问内容</session>"
2289
+ result = _sanitize_user_text(raw)
2290
+ assert "用户真实提问内容" in result
2291
+
2292
+ def test_session_tag_wrapping_with_inner_noise(self):
2293
+ """<session> 内部混合噪声标签时,二次回退应正确剥离噪声."""
2294
+ raw = (
2295
+ "<session>session_key: abc\n"
2296
+ "<system-reminder>噪声内容</system-reminder>"
2297
+ "用户真实输入"
2298
+ "</session>"
2299
+ )
2300
+ result = _sanitize_user_text(raw)
2301
+ assert "用户真实输入" in result
2302
+ assert "噪声内容" not in result
2303
+
2304
+ def test_session_tag_prefix_still_works(self):
2305
+ """用户文本在 <session> 标签之后(原有行为)仍正确."""
2306
+ raw = "<session>metadata</session>用户文本在外部"
2307
+ assert _sanitize_user_text(raw) == "用户文本在外部"
2308
+
2309
+ def test_all_noise_inside_session_tag(self):
2310
+ """<session> 内部全是噪声时,二次回退仍返回空."""
2311
+ raw = "<session><system-reminder>纯噪声</system-reminder></session>"
2312
+ assert _sanitize_user_text(raw) == ""
2313
+
2261
2314
 
2262
2315
  class TestExtractSessionTitle:
2263
2316
  """``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题."""
@@ -2304,14 +2357,16 @@ class TestExtractSessionTitle:
2304
2357
  req = self._build_request([{"role": "user", "content": raw}])
2305
2358
  assert _extract_session_title(req) == "/commit feat: 新增标题清洗"
2306
2359
 
2307
- def test_returns_empty_when_only_noise(self):
2360
+ def test_returns_metadata_fallback_when_only_noise(self):
2361
+ """纯噪声文本回退到 Level 4 元数据兜底(使用 model 名称)."""
2308
2362
  raw = "<system-reminder>纯噪声</system-reminder>"
2309
2363
  req = self._build_request([{"role": "user", "content": raw}])
2310
- assert _extract_session_title(req) == ""
2364
+ assert _extract_session_title(req) == "[Session] test"
2311
2365
 
2312
- def test_returns_empty_for_no_user_messages(self):
2366
+ def test_returns_metadata_fallback_for_no_user_messages(self):
2367
+ """无 user 消息时回退到 Level 4 元数据兜底."""
2313
2368
  req = self._build_request([{"role": "assistant", "content": "你好"}])
2314
- assert _extract_session_title(req) == ""
2369
+ assert _extract_session_title(req) == "[Session] test"
2315
2370
 
2316
2371
  def test_skips_noise_only_part_to_find_real_input(self):
2317
2372
  """首个 user text part 全噪声时,fallback 到下一个非空 user part."""
@@ -2338,3 +2393,266 @@ class TestExtractSessionTitle:
2338
2393
  ]
2339
2394
  req = self._build_request(messages)
2340
2395
  assert _extract_session_title(req) == "新的用户问题"
2396
+
2397
+
2398
+ # ═══════════════════════════════════════════════════════════════════
2399
+ # 多层级回退标题提取测试
2400
+ # ═══════════════════════════════════════════════════════════════════
2401
+
2402
+
2403
+ class TestExtractTitleFromUserText:
2404
+ """Level 1 辅助函数 ``_extract_title_from_user_text``."""
2405
+
2406
+ def test_returns_first_non_empty_user_text(self):
2407
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2408
+
2409
+ msgs = [
2410
+ CanonicalMessagePart(
2411
+ type=CanonicalPartType.TEXT, role="user", text="用户输入"
2412
+ ),
2413
+ ]
2414
+ assert _extract_title_from_user_text(msgs) == "用户输入"
2415
+
2416
+ def test_skips_assistant_text(self):
2417
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2418
+
2419
+ msgs = [
2420
+ CanonicalMessagePart(
2421
+ type=CanonicalPartType.TEXT, role="assistant", text="助手回复"
2422
+ ),
2423
+ CanonicalMessagePart(
2424
+ type=CanonicalPartType.TEXT, role="user", text="用户问题"
2425
+ ),
2426
+ ]
2427
+ assert _extract_title_from_user_text(msgs) == "用户问题"
2428
+
2429
+ def test_returns_empty_for_noise_only(self):
2430
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2431
+
2432
+ msgs = [
2433
+ CanonicalMessagePart(
2434
+ type=CanonicalPartType.TEXT,
2435
+ role="user",
2436
+ text="<system-reminder>纯噪声</system-reminder>",
2437
+ ),
2438
+ ]
2439
+ assert _extract_title_from_user_text(msgs) == ""
2440
+
2441
+
2442
+ class TestExtractTitleFromToolResults:
2443
+ """Level 2 辅助函数 ``_extract_title_from_tool_results``."""
2444
+
2445
+ def test_extracts_tool_result_text(self):
2446
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2447
+
2448
+ msgs = [
2449
+ CanonicalMessagePart(
2450
+ type=CanonicalPartType.TOOL_RESULT,
2451
+ role="user",
2452
+ text="file contents here",
2453
+ ),
2454
+ ]
2455
+ title = _extract_title_from_tool_results(msgs)
2456
+ assert title == "[Tool output] file contents here"
2457
+
2458
+ def test_skips_empty_tool_result(self):
2459
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2460
+
2461
+ msgs = [
2462
+ CanonicalMessagePart(
2463
+ type=CanonicalPartType.TOOL_RESULT, role="user", text=""
2464
+ ),
2465
+ ]
2466
+ assert _extract_title_from_tool_results(msgs) == ""
2467
+
2468
+ def test_truncates_long_tool_result(self):
2469
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2470
+
2471
+ long_text = "A" * 200
2472
+ msgs = [
2473
+ CanonicalMessagePart(
2474
+ type=CanonicalPartType.TOOL_RESULT, role="user", text=long_text
2475
+ ),
2476
+ ]
2477
+ title = _extract_title_from_tool_results(msgs)
2478
+ assert title.startswith("[Tool output] ")
2479
+ assert len(title) <= len("[Tool output] ") + _FALLBACK_TITLE_MAX_LEN
2480
+
2481
+ def test_sanitizes_noise_in_tool_result(self):
2482
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2483
+
2484
+ msgs = [
2485
+ CanonicalMessagePart(
2486
+ type=CanonicalPartType.TOOL_RESULT,
2487
+ role="user",
2488
+ text="<system-reminder>noise</system-reminder>clean output",
2489
+ ),
2490
+ ]
2491
+ title = _extract_title_from_tool_results(msgs)
2492
+ assert title == "[Tool output] clean output"
2493
+
2494
+ def test_returns_empty_when_all_noise(self):
2495
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2496
+
2497
+ msgs = [
2498
+ CanonicalMessagePart(
2499
+ type=CanonicalPartType.TOOL_RESULT,
2500
+ role="user",
2501
+ text="<system-reminder>纯噪声</system-reminder>",
2502
+ ),
2503
+ ]
2504
+ assert _extract_title_from_tool_results(msgs) == ""
2505
+
2506
+
2507
+ class TestExtractTitleFromImages:
2508
+ """Level 3 辅助函数 ``_extract_title_from_images``."""
2509
+
2510
+ def test_single_image(self):
2511
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2512
+
2513
+ msgs = [
2514
+ CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
2515
+ ]
2516
+ assert _extract_title_from_images(msgs) == "[1 Image]"
2517
+
2518
+ def test_multiple_images(self):
2519
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2520
+
2521
+ msgs = [
2522
+ CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
2523
+ CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
2524
+ CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
2525
+ ]
2526
+ assert _extract_title_from_images(msgs) == "[3 Images]"
2527
+
2528
+ def test_no_images(self):
2529
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2530
+
2531
+ msgs = [
2532
+ CanonicalMessagePart(type=CanonicalPartType.TEXT, role="user", text="文本"),
2533
+ ]
2534
+ assert _extract_title_from_images(msgs) == ""
2535
+
2536
+ def test_skips_assistant_images(self):
2537
+ from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
2538
+
2539
+ msgs = [
2540
+ CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="assistant"),
2541
+ ]
2542
+ assert _extract_title_from_images(msgs) == ""
2543
+
2544
+
2545
+ class TestExtractTitleFromMetadata:
2546
+ """Level 4 辅助函数 ``_extract_title_from_metadata``."""
2547
+
2548
+ @staticmethod
2549
+ def _build_request_with_meta(tool_names: list[str] | None = None, model: str = ""):
2550
+ body: dict = {"model": model, "messages": []}
2551
+ if tool_names:
2552
+ body["tools"] = [{"name": n} for n in tool_names]
2553
+ return build_canonical_request(body, {})
2554
+
2555
+ def test_uses_tool_names(self):
2556
+ req = self._build_request_with_meta(
2557
+ tool_names=["Bash", "Read", "Edit"], model="claude-opus-4-8"
2558
+ )
2559
+ assert _extract_title_from_metadata(req) == "[Tool call] Bash, Read, Edit"
2560
+
2561
+ def test_limits_to_three_tool_names(self):
2562
+ req = self._build_request_with_meta(
2563
+ tool_names=["Bash", "Read", "Edit", "Write", "Grep"], model="test"
2564
+ )
2565
+ assert _extract_title_from_metadata(req) == "[Tool call] Bash, Read, Edit"
2566
+
2567
+ def test_uses_model_when_no_tools(self):
2568
+ req = self._build_request_with_meta(tool_names=[], model="claude-sonnet-4-6")
2569
+ assert _extract_title_from_metadata(req) == "[Session] claude-sonnet-4-6"
2570
+
2571
+ def test_returns_empty_when_nothing(self):
2572
+ req = self._build_request_with_meta(tool_names=[], model="")
2573
+ assert _extract_title_from_metadata(req) == ""
2574
+
2575
+
2576
+ class TestExtractSessionTitleFallback:
2577
+ """``_extract_session_title`` 多层级回退集成测试."""
2578
+
2579
+ @staticmethod
2580
+ def _build_request(messages: list[dict], **extra):
2581
+ body: dict = {"model": "test-model", "messages": messages, **extra}
2582
+ return build_canonical_request(body, {})
2583
+
2584
+ def test_level1_takes_priority(self):
2585
+ """Level 1 命中时不回退到 Level 2."""
2586
+ messages = [
2587
+ {
2588
+ "role": "user",
2589
+ "content": [
2590
+ {"type": "text", "text": "用户真实问题"},
2591
+ {
2592
+ "type": "tool_result",
2593
+ "tool_use_id": "tu_1",
2594
+ "content": "工具输出",
2595
+ },
2596
+ ],
2597
+ }
2598
+ ]
2599
+ req = self._build_request(messages)
2600
+ assert _extract_session_title(req) == "用户真实问题"
2601
+
2602
+ def test_level2_when_no_text(self):
2603
+ """无 user TEXT 时,回退到 Level 2 TOOL_RESULT."""
2604
+ messages = [
2605
+ {
2606
+ "role": "user",
2607
+ "content": [
2608
+ {
2609
+ "type": "tool_result",
2610
+ "tool_use_id": "tu_1",
2611
+ "content": [{"type": "text", "text": "文件内容摘要"}],
2612
+ },
2613
+ ],
2614
+ }
2615
+ ]
2616
+ req = self._build_request(messages)
2617
+ assert _extract_session_title(req) == "[Tool output] 文件内容摘要"
2618
+
2619
+ def test_level3_when_only_images(self):
2620
+ """无 TEXT 和 TOOL_RESULT 时,回退到 Level 3 IMAGE."""
2621
+ messages = [
2622
+ {
2623
+ "role": "user",
2624
+ "content": [
2625
+ {
2626
+ "type": "image",
2627
+ "source": {
2628
+ "type": "base64",
2629
+ "media_type": "image/png",
2630
+ "data": "abc",
2631
+ },
2632
+ },
2633
+ ],
2634
+ }
2635
+ ]
2636
+ req = self._build_request(messages)
2637
+ assert _extract_session_title(req) == "[1 Image]"
2638
+
2639
+ def test_level4_uses_tool_names(self):
2640
+ """所有消息级别均无内容时,回退到 Level 4 元数据."""
2641
+ req = self._build_request([], tools=[{"name": "Bash"}, {"name": "Read"}])
2642
+ assert _extract_session_title(req) == "[Tool call] Bash, Read"
2643
+
2644
+ def test_level4_uses_model_name(self):
2645
+ """无 tools 时,Level 4 使用 model 名称."""
2646
+ req = self._build_request([])
2647
+ assert _extract_session_title(req) == "[Session] test-model"
2648
+
2649
+ def test_fallback_cascade_full(self):
2650
+ """Level 1 全噪声 → Level 2 全噪声 → Level 3 无图 → Level 4 模型名."""
2651
+ messages = [
2652
+ {
2653
+ "role": "user",
2654
+ "content": "<system-reminder>纯噪声</system-reminder>",
2655
+ },
2656
+ ]
2657
+ req = self._build_request(messages)
2658
+ assert _extract_session_title(req) == "[Session] test-model"
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "coding-proxy"
77
- version = "0.5.1a4"
77
+ version = "0.5.1a6"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes