coding-proxy 0.2.4a5__tar.gz → 0.3.0a1__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 (181) hide show
  1. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/CHANGELOG.md +5 -0
  2. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/PKG-INFO +8 -8
  3. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/README.md +7 -7
  4. coding_proxy-0.3.0a1/assets/dashboard-v0.2.4.png +0 -0
  5. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/arch/testing.md +1 -1
  6. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/framework.md +2 -11
  7. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/zh-CN/README.md +7 -7
  8. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/pyproject.toml +1 -1
  9. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/config.default.yaml +6 -0
  10. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/schema.py +11 -0
  11. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/vendor_channels.py +177 -9
  12. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/logging/db.py +78 -11
  13. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/logging/stats.py +102 -26
  14. coding_proxy-0.3.0a1/src/coding/proxy/native_api/__init__.py +84 -0
  15. coding_proxy-0.3.0a1/src/coding/proxy/native_api/config.py +84 -0
  16. coding_proxy-0.3.0a1/src/coding/proxy/native_api/extractors/__init__.py +12 -0
  17. coding_proxy-0.3.0a1/src/coding/proxy/native_api/extractors/anthropic.py +111 -0
  18. coding_proxy-0.3.0a1/src/coding/proxy/native_api/extractors/gemini.py +112 -0
  19. coding_proxy-0.3.0a1/src/coding/proxy/native_api/extractors/openai.py +231 -0
  20. coding_proxy-0.3.0a1/src/coding/proxy/native_api/handler.py +485 -0
  21. coding_proxy-0.3.0a1/src/coding/proxy/native_api/operation.py +167 -0
  22. coding_proxy-0.3.0a1/src/coding/proxy/native_api/routes.py +68 -0
  23. coding_proxy-0.3.0a1/src/coding/proxy/native_api/usage_registry.py +279 -0
  24. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/pricing.py +17 -1
  25. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/executor.py +19 -4
  26. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/usage_parser.py +65 -5
  27. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/usage_recorder.py +39 -1
  28. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/server/app.py +26 -0
  29. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/server/routes.py +31 -14
  30. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/anthropic.py +2 -1
  31. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_app_routes.py +17 -20
  32. coding_proxy-0.3.0a1/tests/test_native_api_extractors.py +331 -0
  33. coding_proxy-0.3.0a1/tests/test_native_api_handler.py +374 -0
  34. coding_proxy-0.3.0a1/tests/test_native_api_operation.py +130 -0
  35. coding_proxy-0.3.0a1/tests/test_native_api_routes.py +132 -0
  36. coding_proxy-0.3.0a1/tests/test_parse_usage_gemini.py +196 -0
  37. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_router_executor.py +162 -0
  38. coding_proxy-0.3.0a1/tests/test_token_logger_native_columns.py +334 -0
  39. coding_proxy-0.3.0a1/tests/test_vendor_channels.py +1731 -0
  40. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/uv.lock +1 -1
  41. coding_proxy-0.2.4a5/assets/dashboard-v0.2.3.png +0 -0
  42. coding_proxy-0.2.4a5/src/coding/proxy/server/request_normalizer.py +0 -162
  43. coding_proxy-0.2.4a5/tests/test_request_normalizer.py +0 -811
  44. coding_proxy-0.2.4a5/tests/test_vendor_channels.py +0 -761
  45. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/.github/workflows/ci.yml +0 -0
  46. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/.github/workflows/coverage.yml +0 -0
  47. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/.github/workflows/release.yml +0 -0
  48. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/.gitignore +0 -0
  49. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/.pre-commit-config.yaml +0 -0
  50. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/AGENTS.md +0 -0
  51. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/CLAUDE.md +0 -0
  52. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/LICENSE +0 -0
  53. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/arch/config-reference.md +0 -0
  54. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/arch/convert.md +0 -0
  55. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/arch/design-patterns.md +0 -0
  56. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/arch/routing.md +0 -0
  57. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/arch/vendors.md +0 -0
  58. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/ci-cd.md +0 -0
  59. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/guide/api-reference.md +0 -0
  60. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/guide/cli-reference.md +0 -0
  61. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/guide/dashboard.md +0 -0
  62. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/guide/monitoring.md +0 -0
  63. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/guide/quickstart.md +0 -0
  64. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/guide/vendors.md +0 -0
  65. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/docs/user-guide.md +0 -0
  66. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/__init__.py +0 -0
  67. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/__init__.py +0 -0
  68. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/__main__.py +0 -0
  69. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/__init__.py +0 -0
  70. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/providers/__init__.py +0 -0
  71. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/providers/base.py +0 -0
  72. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/providers/github.py +0 -0
  73. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/providers/google.py +0 -0
  74. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/runtime.py +0 -0
  75. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/auth/store.py +0 -0
  76. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/cli/__init__.py +0 -0
  77. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/cli/auth_commands.py +0 -0
  78. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/cli/banner.py +0 -0
  79. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/compat/__init__.py +0 -0
  80. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/compat/canonical.py +0 -0
  81. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/compat/session_store.py +0 -0
  82. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/__init__.py +0 -0
  83. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/auth_schema.py +0 -0
  84. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/loader.py +0 -0
  85. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/resiliency.py +0 -0
  86. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/routing.py +0 -0
  87. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/server.py +0 -0
  88. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/config/vendors.py +0 -0
  89. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/__init__.py +0 -0
  90. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  91. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  92. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  93. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  94. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  95. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/logging/__init__.py +0 -0
  96. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/logging/formatters.py +0 -0
  97. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/__init__.py +0 -0
  98. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/auth.py +0 -0
  99. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/compat.py +0 -0
  100. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/constants.py +0 -0
  101. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/pricing.py +0 -0
  102. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/token.py +0 -0
  103. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/model/vendor.py +0 -0
  104. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/__init__.py +0 -0
  105. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  106. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/error_classifier.py +0 -0
  107. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/model_mapper.py +0 -0
  108. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/quota_guard.py +0 -0
  109. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/rate_limit.py +0 -0
  110. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/retry.py +0 -0
  111. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/router.py +0 -0
  112. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/session_manager.py +0 -0
  113. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/routing/tier.py +0 -0
  114. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/server/__init__.py +0 -0
  115. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/server/dashboard.py +0 -0
  116. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/server/factory.py +0 -0
  117. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/server/responses.py +0 -0
  118. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/streaming/__init__.py +0 -0
  119. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  120. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/__init__.py +0 -0
  121. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/alibaba.py +0 -0
  122. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/antigravity.py +0 -0
  123. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/base.py +0 -0
  124. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/copilot.py +0 -0
  125. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/copilot_models.py +0 -0
  126. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  127. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  128. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/doubao.py +0 -0
  129. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/kimi.py +0 -0
  130. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/minimax.py +0 -0
  131. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/mixins.py +0 -0
  132. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  133. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/token_manager.py +0 -0
  134. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/xiaomi.py +0 -0
  135. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/src/coding/proxy/vendors/zhipu.py +0 -0
  136. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/__init__.py +0 -0
  137. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_antigravity.py +0 -0
  138. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_auto_login.py +0 -0
  139. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_banner.py +0 -0
  140. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_circuit_breaker.py +0 -0
  141. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_cli_usage.py +0 -0
  142. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_compat.py +0 -0
  143. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_config_init.py +0 -0
  144. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_config_loader.py +0 -0
  145. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_convert_request.py +0 -0
  146. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_convert_response.py +0 -0
  147. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_convert_sse.py +0 -0
  148. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_copilot.py +0 -0
  149. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_copilot_convert_request.py +0 -0
  150. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_copilot_convert_response.py +0 -0
  151. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_copilot_models.py +0 -0
  152. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_copilot_urls.py +0 -0
  153. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_currency.py +0 -0
  154. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_error_classifier.py +0 -0
  155. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_logging_dual_write.py +0 -0
  156. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_mixins.py +0 -0
  157. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_auth.py +0 -0
  158. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_compat.py +0 -0
  159. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_constants.py +0 -0
  160. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_mapper.py +0 -0
  161. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_pricing.py +0 -0
  162. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_token.py +0 -0
  163. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_model_vendor.py +0 -0
  164. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_native_vendors.py +0 -0
  165. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_parse_usage.py +0 -0
  166. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_pricing.py +0 -0
  167. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_quota_guard.py +0 -0
  168. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_rate_limit.py +0 -0
  169. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_router_chain.py +0 -0
  170. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_runtime_reauth.py +0 -0
  171. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_schema.py +0 -0
  172. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_streaming_anthropic_compat.py +0 -0
  173. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_tier.py +0 -0
  174. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_tiers_config.py +0 -0
  175. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_time_range.py +0 -0
  176. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_token_logger.py +0 -0
  177. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_token_manager.py +0 -0
  178. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_types.py +0 -0
  179. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_vendor_streaming.py +0 -0
  180. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_vendors.py +0 -0
  181. {coding_proxy-0.2.4a5 → coding_proxy-0.3.0a1}/tests/test_zhipu.py +0 -0
@@ -4,6 +4,11 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ - feat(native-api): 新增 `/api/{openai,gemini,anthropic}/**` 原生 LLM API 全量 catch-all 透传通道——客户端只需改 SDK `base_url` 即可复用 proxy 链路访问 OpenAI / Gemini / Anthropic 官方 API,认证完全透传不保管凭据;核心由 `NativeProxyHandler` + `OperationClassifier` + `NativeUsageExtractor` Registry 三件套组成,与既有 `/v1/messages` Claude Code 链路正交共存、零回归;首版覆盖 chat / completions / responses / embeddings / audio / image / count_tokens / moderations / cachedContents / messages / batches 等全量端点;
8
+ - feat(usage): `usage_log` 新增 `client_category` / `operation` / `endpoint` / `extra_usage_json` 四列(全部 `DEFAULT`、幂等迁移),区分 Claude Code 场景(`'cc'`)与原生 API 场景(`'api'`),承载规范化操作名与非规范 token 字段(reasoning / audio / thoughts / server_tool_use 等);`query_usage` 支持按新列过滤,`_PERIOD_SQL` 聚合追加 `client_category, operation` 维度;
9
+ - feat(usage-parser): `parse_usage_from_chunk` 扩展识别 Gemini SSE `usageMetadata.*`(promptTokenCount / candidatesTokenCount / cachedContentTokenCount / thoughtsTokenCount / toolUsePromptTokenCount),新增 `gemini_usage_metadata` evidence kind,既有 Anthropic/OpenAI 分支行为零变更;
10
+ - refactor(vendor-channels): 彻底收敛跨供应商兼容性逻辑——删除 `server/request_normalizer.py` 入口通用规范化层,将 `srvtoolu_*` ID 重写、`server_tool_use_delta` 私有块剥离全部迁入源→目标绑定通道(`prepare_zhipu_to_anthropic`、`prepare_zhipu_to_copilot`);新增 `infer_source_vendor_from_body` 内容感知源推断,在无会话状态的首次请求场景下兜底识别源供应商;`_RouteExecutor._determine_source_vendor` 扩充为三级优先级(failed_tier → session_state → body inference),确保未注册转换对不触发任何清洗;
11
+ - refactor(count-tokens): `/v1/messages/count_tokens` 端点移除无条件 `strip_thinking_blocks` 过度防御,改为基于 `infer_source_vendor_from_body` + `get_transition_channel` 的按需通道清洗,语义与 `/v1/messages` 对齐;
7
12
  - fix(request-normalizer): 重设计 zhipu→anthropic 跨供应商 tool_use/tool_result 配对修复——以单遍自包含 `enforce_anthropic_tool_pairing` 替代原有多步串联管线(剥离→重定位→孤儿修复),消除步骤间隐式依赖导致的孤儿 tool_use 漏修问题,彻底根治 `tool_use ids were found without tool_result blocks` 400 异常;
8
13
  - refactor(vendor-channels): 将供应商转换通道从「目标 vendor 专属」重构为「源→目标绑定」模型——注册表键从 `target_vendor` 改为 `(source, target)` 二元组,通道函数从 `prepare_for_X` 重命名为 `prepare_X_to_Y`,触发逻辑从 `_needs_vendor_channel` 替换为 `_determine_source_vendor`(基于请求内 `failed_tier_name` 和会话历史推断源 vendor),未注册的转换对(如 anthropic→zhipu)不触发任何通道;
9
14
  - feat(vendor-channels): 新增 zhipu→anthropic、zhipu→copilot、copilot→zhipu 三条源→目标绑定转换通道,在跨供应商故障转移时自动清理源 vendor 产物(thinking 块、cache_control 字段、thinking 参数、tool_use/tool_result 配对),消除 `likely format incompatibility (400 + tool_results)` 错误;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.2.4a5
3
+ Version: 0.3.0a1
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
@@ -57,7 +57,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
57
57
  ## 🌟 Core Features
58
58
 
59
59
  <div align="center">
60
- <img src="assets/dashboard-v0.2.3.png">
60
+ <img src="assets/dashboard-v0.2.4.png">
61
61
  </div>
62
62
 
63
63
  - **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Google Antigravity, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
@@ -119,13 +119,13 @@ claude
119
119
 
120
120
  `coding-proxy` comes equipped with a badass suite of CLI tools to help you boss around your proxy state.
121
121
 
122
- | Command | Description | Example Usage |
123
- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
124
- | `start` | **Fire up the proxy server.** Supports custom ports and configuration paths. | `coding-proxy start -p 8080 -c ~/config.yaml` |
122
+ | Command | Description | Example Usage |
123
+ | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
124
+ | `start` | **Fire up the proxy server.** Supports custom ports and configuration paths. | `coding-proxy start -p 8080 -c ~/config.yaml` |
125
125
  | `auth` | **Manage OAuth credentials.** Sub-commands: `login` (browser OAuth), `status` (token validity), `reauth` (re-authenticate), `logout` (clear tokens). | `coding-proxy auth login -p github` |
126
- | `status` | **Check proxy health.** Shows circuit breaker states (OPEN/CLOSED) and quota status across all tiers. | `coding-proxy status` |
127
- | `usage` | **Token Stats Dashboard.** Stalks every single token consumed, failovers triggered, and latency across day/vendor/model dimensions. | `coding-proxy usage -d 7 -v anthropic` |
128
- | `reset` | **The emergency flush button.** Force-reset all circuit breakers and quotas instantly when you've confirmed the main vendor is back from the dead. | `coding-proxy reset` |
126
+ | `status` | **Check proxy health.** Shows circuit breaker states (OPEN/CLOSED) and quota status across all tiers. | `coding-proxy status` |
127
+ | `usage` | **Token Stats Dashboard.** Stalks every single token consumed, failovers triggered, and latency across day/vendor/model dimensions. | `coding-proxy usage -d 7 -v anthropic` |
128
+ | `reset` | **The emergency flush button.** Force-reset all circuit breakers and quotas instantly when you've confirmed the main vendor is back from the dead. | `coding-proxy reset` |
129
129
 
130
130
  ---
131
131
 
@@ -30,7 +30,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
30
30
  ## 🌟 Core Features
31
31
 
32
32
  <div align="center">
33
- <img src="assets/dashboard-v0.2.3.png">
33
+ <img src="assets/dashboard-v0.2.4.png">
34
34
  </div>
35
35
 
36
36
  - **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Google Antigravity, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
@@ -92,13 +92,13 @@ claude
92
92
 
93
93
  `coding-proxy` comes equipped with a badass suite of CLI tools to help you boss around your proxy state.
94
94
 
95
- | Command | Description | Example Usage |
96
- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
97
- | `start` | **Fire up the proxy server.** Supports custom ports and configuration paths. | `coding-proxy start -p 8080 -c ~/config.yaml` |
95
+ | Command | Description | Example Usage |
96
+ | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
97
+ | `start` | **Fire up the proxy server.** Supports custom ports and configuration paths. | `coding-proxy start -p 8080 -c ~/config.yaml` |
98
98
  | `auth` | **Manage OAuth credentials.** Sub-commands: `login` (browser OAuth), `status` (token validity), `reauth` (re-authenticate), `logout` (clear tokens). | `coding-proxy auth login -p github` |
99
- | `status` | **Check proxy health.** Shows circuit breaker states (OPEN/CLOSED) and quota status across all tiers. | `coding-proxy status` |
100
- | `usage` | **Token Stats Dashboard.** Stalks every single token consumed, failovers triggered, and latency across day/vendor/model dimensions. | `coding-proxy usage -d 7 -v anthropic` |
101
- | `reset` | **The emergency flush button.** Force-reset all circuit breakers and quotas instantly when you've confirmed the main vendor is back from the dead. | `coding-proxy reset` |
99
+ | `status` | **Check proxy health.** Shows circuit breaker states (OPEN/CLOSED) and quota status across all tiers. | `coding-proxy status` |
100
+ | `usage` | **Token Stats Dashboard.** Stalks every single token consumed, failovers triggered, and latency across day/vendor/model dimensions. | `coding-proxy usage -d 7 -v anthropic` |
101
+ | `reset` | **The emergency flush button.** Force-reset all circuit breakers and quotas instantly when you've confirmed the main vendor is back from the dead. | `coding-proxy reset` |
102
102
 
103
103
  ---
104
104
 
@@ -65,6 +65,7 @@
65
65
  | `test_convert_sse.py` | Gemini SSE→Anthropic SSE 流适配(单/多 chunk、各 finishReason、边界情况) |
66
66
  | `test_copilot_convert_request.py` | Anthropic→OpenAI 请求格式转换 |
67
67
  | `test_copilot_convert_response.py` | OpenAI→Anthropic 响应格式转换 |
68
+ | `test_vendor_channels.py` | 源→目标通道:zhipu/copilot 兼容性清洗、tool_use 配对、内容感知源推断 |
68
69
 
69
70
  ### 2.5 数据模型(model)
70
71
 
@@ -95,7 +96,6 @@
95
96
  | 测试文件 | 覆盖范围 |
96
97
  | ---------------------------- | ------------------------------------------------------- |
97
98
  | `test_app_routes.py` | FastAPI 路由端点测试 |
98
- | `test_request_normalizer.py` | 请求标准化:私有块清洗、tool_use_id 重写、fatal_reasons |
99
99
  | `test_cli_usage.py` | CLI 用量查询命令 |
100
100
  | `test_banner.py` | CLI Banner 显示 |
101
101
  | `test_logging_dual_write.py` | 日志双写机制 |
@@ -69,7 +69,6 @@ graph TD
69
69
  App["<code>app.py</code><br/>应用工厂 + lifespan"]
70
70
  Routes["<code>routes.py</code><br/>路由注册"]
71
71
  Factory["<code>factory.py</code><br/>Vendor/Tier 构建工厂"]
72
- Normalizer["<code>request_normalizer.py</code><br/>请求标准化"]
73
72
  Dashboard["<code>dashboard.py</code><br/>状态面板"]
74
73
  end
75
74
 
@@ -179,15 +178,8 @@ graph TD
179
178
  ```mermaid
180
179
  flowchart TD
181
180
  Client["Client POST /v1/messages"] --> Server["server.routes.messages()"]
182
-
183
- subgraph Normalize ["请求标准化"]
184
- Body["body = await request.json()"]
185
- Norm["normalize_anthropic_request(body)<br/>清洗私有块 + 重写 tool_use_id"]
186
- Body --> Norm
187
- end
188
-
189
- Server --> Normalize
190
- Norm --> RouteType{"stream?"}
181
+ Server --> Body["body = await request.json()"]
182
+ Body --> RouteType{"stream?"}
191
183
 
192
184
  RouteType -- "true" --> StreamRoute["route_stream()"]
193
185
  RouteType -- "false" --> MsgRoute["route_message()"]
@@ -401,7 +393,6 @@ flowchart TD
401
393
  | [`app.py`](../src/coding/proxy/server/app.py) | FastAPI 应用工厂 `create_app()` + `lifespan` 生命周期管理 |
402
394
  | [`factory.py`](../src/coding/proxy/server/factory.py) | Vendor/Tier 构建工厂 + 凭证解析 |
403
395
  | [`routes.py`](../src/coding/proxy/server/routes.py) | 路由端点按职责分组注册 |
404
- | [`request_normalizer.py`](../src/coding/proxy/server/request_normalizer.py) | 入站请求标准化(清洗供应商私有块) |
405
396
  | [`responses.py`](../src/coding/proxy/server/responses.py) | 响应辅助工具(JSON error / stream error 构建) |
406
397
  | [`dashboard.py`](../src/coding/proxy/server/dashboard.py) | 状态面板(Web Dashboard) |
407
398
 
@@ -30,7 +30,7 @@
30
30
  ## 🌟 核心特性 (Core Features)
31
31
 
32
32
  <div align="center">
33
- <img src="../../assets/dashboard-v0.2.3.png">
33
+ <img src="../../assets/dashboard-v0.2.4.png">
34
34
  </div>
35
35
 
36
36
  - **⛓️ N-tier 链式故障转移 (Failover)**:自主降序序列,支持 Claude 官方 Plans,以及 GitHub Copilot、Google Antigravity、智谱、MiniMax、阿里千问、小米、Kimi、豆包等的 Coding Plan。
@@ -92,13 +92,13 @@ claude
92
92
 
93
93
  `coding-proxy` 附带了强大的 CLI 工具套件,帮助您全面掌控代理状态。
94
94
 
95
- | 指令 | 说明 | 示例用法 |
96
- | :------- | :-------------------------------------------------------------------------------- | :-------------------------------------------- |
97
- | `start` | **启动代理服务器**。支持自定义端口与配置路径。 | `coding-proxy start -p 8080 -c ~/config.yaml` |
95
+ | 指令 | 说明 | 示例用法 |
96
+ | :------- | :------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------- |
97
+ | `start` | **启动代理服务器**。支持自定义端口与配置路径。 | `coding-proxy start -p 8080 -c ~/config.yaml` |
98
98
  | `auth` | **管理 OAuth 登录凭证**。子命令:`login`(浏览器 OAuth 登录)、`status`(令牌状态)、`reauth`(重认证)、`logout`(清除令牌)。 | `coding-proxy auth login -p github` |
99
- | `status` | **查看代理健康状态**。展示各层级熔断器(OPEN/CLOSED)与配额状态。 | `coding-proxy status` |
100
- | `usage` | **Token 统计看板**。按天/供应商/模型维度追踪每一次的 Token 消耗、故障转移及耗时。 | `coding-proxy usage -d 7 -v anthropic` |
101
- | `reset` | **强制一键重置**。人工确认主供应商恢复可用后,立刻初始化所有熔断器和配额状态。 | `coding-proxy reset` |
99
+ | `status` | **查看代理健康状态**。展示各层级熔断器(OPEN/CLOSED)与配额状态。 | `coding-proxy status` |
100
+ | `usage` | **Token 统计看板**。按天/供应商/模型维度追踪每一次的 Token 消耗、故障转移及耗时。 | `coding-proxy usage -d 7 -v anthropic` |
101
+ | `reset` | **强制一键重置**。人工确认主供应商恢复可用后,立刻初始化所有熔断器和配额状态。 | `coding-proxy reset` |
102
102
 
103
103
  ---
104
104
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.2.4a5"
3
+ version = "0.3.0a1"
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"
@@ -331,6 +331,12 @@ model_mapping:
331
331
  # 未配置定价的模型在统计中 Cost 列显示 "-"
332
332
  pricing:
333
333
  # ── Anthropic Claude ──
334
+ - vendor: anthropic
335
+ model: claude-opus-4-7
336
+ input_cost_per_mtok: $5.0
337
+ output_cost_per_mtok: $25.0
338
+ cache_write_cost_per_mtok: $6.25
339
+ cache_read_cost_per_mtok: $0.50
334
340
  - vendor: anthropic
335
341
  model: claude-opus-4-6
336
342
  input_cost_per_mtok: $5.0
@@ -19,6 +19,7 @@ from typing import Any
19
19
 
20
20
  from pydantic import BaseModel, Field, model_validator
21
21
 
22
+ from ..native_api.config import NativeApiConfig # noqa: F401
22
23
  from .auth_schema import AuthConfig # noqa: F401
23
24
  from .resiliency import ( # noqa: F401
24
25
  CircuitBreakerConfig,
@@ -143,6 +144,14 @@ class ProxyConfig(BaseModel):
143
144
  "未配置时回退到 vendors 列表原始顺序。"
144
145
  ),
145
146
  )
147
+ # 原生 LLM API 透传通道 — 与 /v1/messages(Claude Code)链路正交
148
+ native_api: NativeApiConfig = Field(
149
+ default_factory=NativeApiConfig,
150
+ description=(
151
+ "OpenAI / Gemini / Anthropic 原生 API 透传配置。"
152
+ "三个 provider 默认 enabled=False,显式启用才暴露 /api/{provider}/* 端点。"
153
+ ),
154
+ )
146
155
 
147
156
  @model_validator(mode="before")
148
157
  @classmethod
@@ -320,4 +329,6 @@ __all__ = [
320
329
  "DoubaoConfig",
321
330
  "XiaomiConfig",
322
331
  "AlibabaConfig",
332
+ # native api passthrough
333
+ "NativeApiConfig",
323
334
  ]
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import copy
18
18
  import logging
19
+ import re
19
20
  from collections.abc import Callable
20
21
  from typing import Any
21
22
 
@@ -23,6 +24,17 @@ logger = logging.getLogger(__name__)
23
24
 
24
25
  _THINKING_BLOCK_TYPES = {"thinking", "redacted_thinking"}
25
26
 
27
+ # ── Anthropic 工具块 ID 规范 ───────────────────────────────────
28
+ _ANTHROPIC_TOOL_USE_ID_RE = re.compile(r"^toolu_[A-Za-z0-9_]+$")
29
+ _ANTHROPIC_SERVER_TOOL_USE_ID_RE = re.compile(r"^srvtoolu_[A-Za-z0-9_]+$")
30
+
31
+ # Zhipu 流式响应中出现的非标准供应商私有 content block 类型.
32
+ # Anthropic API 拒绝这些块,需要在跨 vendor 请求体中剥离.
33
+ _ZHIPU_VENDOR_BLOCK_TYPES = {"server_tool_use_delta"}
34
+
35
+ # Zhipu 内联输出非标准 content block 类型的标识(用于源供应商推断).
36
+ _ZHIPU_SERVER_TOOL_USE_TYPES = {"server_tool_use", "server_tool_use_delta"}
37
+
26
38
  # ── 转换通道注册表 ─────────────────────────────────────────────
27
39
  # (source_vendor, target_vendor) → (body) → (prepared_body, adaptations)
28
40
  VENDOR_TRANSITIONS: dict[
@@ -250,6 +262,138 @@ def _strip_cache_control(body: dict[str, Any]) -> int:
250
262
  return removed
251
263
 
252
264
 
265
+ def _remove_vendor_blocks(body: dict[str, Any], block_types: set[str]) -> int:
266
+ """从 messages[].content[] 中就地移除指定 type 的内容块.
267
+
268
+ 用于剥离 vendor 私有 content block 类型(如 zhipu 的 ``server_tool_use_delta``),
269
+ Anthropic API 会拒绝这些非标准块。
270
+
271
+ Returns:
272
+ 被移除的块数量。
273
+ """
274
+ removed = 0
275
+ for message in body.get("messages", []):
276
+ if not isinstance(message, dict):
277
+ continue
278
+ content = message.get("content")
279
+ if not isinstance(content, list):
280
+ continue
281
+ new_content: list[Any] = []
282
+ for block in content:
283
+ if isinstance(block, dict) and block.get("type") in block_types:
284
+ removed += 1
285
+ continue
286
+ new_content.append(block)
287
+ if removed:
288
+ message["content"] = new_content
289
+ return removed
290
+
291
+
292
+ def _rewrite_srvtoolu_ids(body: dict[str, Any]) -> tuple[int, dict[str, str]]:
293
+ """将 zhipu 的 ``server_tool_use`` + ``srvtoolu_*`` ID 改写为标准 Anthropic 形式.
294
+
295
+ Anthropic API 要求 tool_use 类型与 ``toolu_*`` 格式的 ID。Zhipu 的
296
+ ``server_tool_use`` + ``srvtoolu_*`` 在上游 Anthropic 兼容端点可用,但无法
297
+ 透传至其他供应商;同时还需重写紧随其后 user 消息中 ``tool_result.tool_use_id``
298
+ 引用,保持配对关系。
299
+
300
+ Returns:
301
+ (rewritten_count, id_map) — 重写次数与 {原 ID: 新 ID} 映射。
302
+ """
303
+ id_map: dict[str, str] = {}
304
+ counter = 0
305
+
306
+ def next_id() -> str:
307
+ nonlocal counter
308
+ counter += 1
309
+ return f"toolu_normalized_{counter}"
310
+
311
+ for message in body.get("messages", []):
312
+ if not isinstance(message, dict):
313
+ continue
314
+ content = message.get("content")
315
+ if not isinstance(content, list):
316
+ continue
317
+ role = message.get("role")
318
+ for block in content:
319
+ if not isinstance(block, dict):
320
+ continue
321
+ block_type = block.get("type")
322
+ block_id = block.get("id")
323
+
324
+ # Case A: assistant 消息里的 server_tool_use / srvtoolu_* → 改写
325
+ if role == "assistant" and block_type in {"tool_use", "server_tool_use"}:
326
+ if isinstance(block_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
327
+ block_id
328
+ ):
329
+ new_id = next_id()
330
+ id_map[block_id] = new_id
331
+ block["id"] = new_id
332
+ block["type"] = "tool_use"
333
+ elif (
334
+ isinstance(block_id, str)
335
+ and block_id
336
+ and not _ANTHROPIC_TOOL_USE_ID_RE.match(block_id)
337
+ and block.get("name")
338
+ ):
339
+ # 非标准 ID(非 toolu_ / srvtoolu_),且具备 name 可改写
340
+ new_id = next_id()
341
+ id_map[block_id] = new_id
342
+ block["id"] = new_id
343
+ block["type"] = "tool_use"
344
+ elif block_type == "server_tool_use" and isinstance(block_id, str):
345
+ # 兜底: 类型是 server_tool_use 但 ID 已是标准 toolu_ 形式,仅纠正类型
346
+ block["type"] = "tool_use"
347
+
348
+ # Case B: user 消息里的 tool_result.tool_use_id 同步重写
349
+ if block_type == "tool_result":
350
+ tool_use_id = block.get("tool_use_id")
351
+ if isinstance(tool_use_id, str) and tool_use_id in id_map:
352
+ block["tool_use_id"] = id_map[tool_use_id]
353
+
354
+ return len(id_map), id_map
355
+
356
+
357
+ def infer_source_vendor_from_body(body: dict[str, Any]) -> str | None:
358
+ """从请求 body 内容推断源供应商(仅在无会话上下文时作为兜底).
359
+
360
+ 启发式(按置信度排序):
361
+ - 出现 ``srvtoolu_*`` 格式的 ``tool_use.id`` → zhipu
362
+ - 出现 ``server_tool_use`` / ``server_tool_use_delta`` 类型的 content block → zhipu
363
+
364
+ 原则: 只读扫描不修改 body;无匹配返回 None(视作纯净无需跨供应商清洗)。
365
+
366
+ Args:
367
+ body: Anthropic Messages 请求体。
368
+
369
+ Returns:
370
+ 推断的源供应商名称(当前仅支持 ``"zhipu"``),无法推断返回 None。
371
+ """
372
+ for message in body.get("messages", []):
373
+ if not isinstance(message, dict):
374
+ continue
375
+ content = message.get("content")
376
+ if not isinstance(content, list):
377
+ continue
378
+ for block in content:
379
+ if not isinstance(block, dict):
380
+ continue
381
+ block_type = block.get("type")
382
+ if block_type in _ZHIPU_SERVER_TOOL_USE_TYPES:
383
+ return "zhipu"
384
+ block_id = block.get("id")
385
+ if isinstance(block_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
386
+ block_id
387
+ ):
388
+ return "zhipu"
389
+ tool_use_id = block.get("tool_use_id")
390
+ if isinstance(tool_use_id, str) and _ANTHROPIC_SERVER_TOOL_USE_ID_RE.match(
391
+ tool_use_id
392
+ ):
393
+ return "zhipu"
394
+ return None
395
+
396
+
253
397
  # ── copilot → zhipu 转换通道 ─────────────────────────────────────
254
398
 
255
399
 
@@ -316,17 +460,27 @@ def prepare_zhipu_to_copilot(
316
460
  prepared = copy.deepcopy(body)
317
461
  adaptations: list[str] = []
318
462
 
319
- # Step 1: 剥离 thinking/redacted_thinking
463
+ # Step 1: 剥离 zhipu 私有 content block 类型
464
+ removed_vendor_blocks = _remove_vendor_blocks(prepared, _ZHIPU_VENDOR_BLOCK_TYPES)
465
+ if removed_vendor_blocks:
466
+ adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
467
+
468
+ # Step 2: 改写 srvtoolu_* ID 与 server_tool_use 类型
469
+ rewritten, _ = _rewrite_srvtoolu_ids(prepared)
470
+ if rewritten:
471
+ adaptations.append(f"rewritten_{rewritten}_srvtoolu_ids")
472
+
473
+ # Step 3: 剥离 thinking/redacted_thinking 块
320
474
  stripped = strip_thinking_blocks(prepared)
321
475
  if stripped:
322
476
  adaptations.append(f"stripped_{stripped}_thinking_blocks")
323
477
 
324
- # Step 2: 移除 cache_control 字段
478
+ # Step 4: 移除 cache_control 字段
325
479
  removed_cc = _strip_cache_control(prepared)
326
480
  if removed_cc:
327
481
  adaptations.append(f"removed_{removed_cc}_cache_control_fields")
328
482
 
329
- # Step 3: 强制 tool_use/tool_result 配对
483
+ # Step 5: 强制 tool_use/tool_result 配对
330
484
  pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
331
485
  if pairing_fixes:
332
486
  adaptations.extend(pairing_fixes)
@@ -343,14 +497,18 @@ def prepare_zhipu_to_anthropic(
343
497
  """zhipu → anthropic 转换: 清理 zhipu 产物以适配 Anthropic API.
344
498
 
345
499
  Anthropic API 要求:
500
+ - tool_use 类型与 ``toolu_*`` 格式 ID(zhipu 的 ``server_tool_use``/``srvtoolu_*`` 不兼容)
346
501
  - 每个 tool_use 必须在紧随的 user 消息中有对应 tool_result
347
502
  - thinking blocks 的 signature 必须是 Anthropic 签发(zhipu 签发的无效)
503
+ - 不接受 ``server_tool_use_delta`` 等 zhipu 私有流式块类型
348
504
 
349
- 此通道执行两项变换:
350
- 1. enforce_anthropic_tool_pairing: 单遍正向扫描修复配对
351
- 2. strip_thinking_blocks: 移除非 Anthropic 签发的 thinking
505
+ 此通道按顺序执行:
506
+ 1. 剥离 zhipu 私有 block 类型(``server_tool_use_delta``)
507
+ 2. 改写 ``srvtoolu_*`` ID ``server_tool_use`` 类型为标准 Anthropic 形式
508
+ 3. 强制 tool_use/tool_result 配对(单遍正向扫描)
509
+ 4. 剥离 thinking blocks(signature 无效)
352
510
 
353
- 两项变换均为幂等操作,安全地在已清理的请求体上重复执行。
511
+ 所有变换均为幂等操作,安全地在已清理的请求体上重复执行。
354
512
 
355
513
  Returns:
356
514
  (prepared_body, adaptations) — adaptations 为应用的变换描述列表。
@@ -358,12 +516,22 @@ def prepare_zhipu_to_anthropic(
358
516
  prepared = copy.deepcopy(body)
359
517
  adaptations: list[str] = []
360
518
 
361
- # Step 1: 强制 tool_use/tool_result 配对
519
+ # Step 1: 剥离 zhipu 私有 content block 类型(如 server_tool_use_delta)
520
+ removed_vendor_blocks = _remove_vendor_blocks(prepared, _ZHIPU_VENDOR_BLOCK_TYPES)
521
+ if removed_vendor_blocks:
522
+ adaptations.append(f"removed_{removed_vendor_blocks}_zhipu_vendor_blocks")
523
+
524
+ # Step 2: 改写 srvtoolu_* ID 与 server_tool_use 类型
525
+ rewritten, _ = _rewrite_srvtoolu_ids(prepared)
526
+ if rewritten:
527
+ adaptations.append(f"rewritten_{rewritten}_srvtoolu_ids")
528
+
529
+ # Step 3: 强制 tool_use/tool_result 配对
362
530
  pairing_fixes = enforce_anthropic_tool_pairing(prepared.get("messages", []))
363
531
  if pairing_fixes:
364
532
  adaptations.extend(pairing_fixes)
365
533
 
366
- # Step 2: 剥离 thinking blocks(zhipu signature 无效)
534
+ # Step 4: 剥离 thinking blocks(zhipu signature 无效)
367
535
  stripped = strip_thinking_blocks(prepared)
368
536
  if stripped:
369
537
  adaptations.append(f"stripped_{stripped}_thinking_blocks")
@@ -166,7 +166,11 @@ CREATE TABLE IF NOT EXISTS usage_log (
166
166
  success BOOLEAN NOT NULL DEFAULT 1,
167
167
  failover BOOLEAN NOT NULL DEFAULT 0,
168
168
  failover_from TEXT DEFAULT NULL,
169
- request_id TEXT DEFAULT ''
169
+ request_id TEXT DEFAULT '',
170
+ client_category TEXT NOT NULL DEFAULT 'cc',
171
+ operation TEXT NOT NULL DEFAULT '',
172
+ endpoint TEXT NOT NULL DEFAULT '',
173
+ extra_usage_json TEXT NOT NULL DEFAULT '{}'
170
174
  );
171
175
  CREATE TABLE IF NOT EXISTS usage_evidence (
172
176
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -188,6 +192,8 @@ CREATE TABLE IF NOT EXISTS usage_evidence (
188
192
  _CREATE_INDEXES = """
189
193
  CREATE INDEX IF NOT EXISTS idx_usage_ts ON usage_log(ts);
190
194
  CREATE INDEX IF NOT EXISTS idx_usage_vendor ON usage_log(vendor);
195
+ CREATE INDEX IF NOT EXISTS idx_usage_client_category ON usage_log(client_category);
196
+ CREATE INDEX IF NOT EXISTS idx_usage_operation ON usage_log(operation);
191
197
  CREATE INDEX IF NOT EXISTS idx_usage_evidence_request_id ON usage_evidence(request_id);
192
198
  CREATE INDEX IF NOT EXISTS idx_usage_evidence_vendor ON usage_evidence(vendor);
193
199
  """
@@ -196,25 +202,29 @@ CREATE INDEX IF NOT EXISTS idx_usage_evidence_vendor ON usage_evidence(vendor);
196
202
 
197
203
  _PERIOD_SQL: dict[TimePeriod, tuple[str, str, str]] = {
198
204
  # (date_expr, group_by, order_by_expr)
205
+ # group_by / order_by 附带 client_category, operation:
206
+ # 1) 新列加入 SELECT 后必须对应 GROUP BY,否则 SQLite 会返回任意未确定值;
207
+ # 2) 历史行默认 ('cc', '') → 对既有聚合口径零回归(同值无额外拆分);
208
+ # 3) 当 client_category='api' 场景产生混合数据时,按类别 + 操作拆行可直观区分。
199
209
  TimePeriod.DAY: (
200
210
  "local_date(ts) AS date",
201
- "local_date(ts), vendor, model_served",
202
- "local_date(ts) DESC, vendor, model_served",
211
+ "local_date(ts), vendor, model_served, client_category, operation",
212
+ "local_date(ts) DESC, vendor, model_served, client_category, operation",
203
213
  ),
204
214
  TimePeriod.WEEK: (
205
215
  "local_week(ts) AS date",
206
- "local_week(ts), vendor, model_served",
207
- "local_week(ts) DESC, vendor, model_served",
216
+ "local_week(ts), vendor, model_served, client_category, operation",
217
+ "local_week(ts) DESC, vendor, model_served, client_category, operation",
208
218
  ),
209
219
  TimePeriod.MONTH: (
210
220
  "local_month(ts) AS date",
211
- "local_month(ts), vendor, model_served",
212
- "local_month(ts) DESC, vendor, model_served",
221
+ "local_month(ts), vendor, model_served, client_category, operation",
222
+ "local_month(ts) DESC, vendor, model_served, client_category, operation",
213
223
  ),
214
224
  TimePeriod.TOTAL: (
215
225
  "NULL AS date",
216
- "vendor, model_served",
217
- "vendor, model_served",
226
+ "vendor, model_served, client_category, operation",
227
+ "vendor, model_served, client_category, operation",
218
228
  ),
219
229
  }
220
230
 
@@ -236,6 +246,7 @@ class TokenLogger:
236
246
  # 迁移必须在建索引之前执行,确保 vendor 列已存在
237
247
  await self._migrate_rename_backend_to_vendor()
238
248
  await self._migrate_add_failover_from()
249
+ await self._migrate_add_native_columns()
239
250
  await self._db.executescript(_CREATE_INDEXES)
240
251
  # 注册时区感知的日期函数:将 UTC 时间戳转为本地时间维度
241
252
  await self._db.create_function("local_date", 1, _local_date_udf)
@@ -255,6 +266,26 @@ class TokenLogger:
255
266
  )
256
267
  logger.info("Migration: added failover_from column to usage_log")
257
268
 
269
+ async def _migrate_add_native_columns(self) -> None:
270
+ """幂等迁移:为已有数据库添加原生 API 透传通道所需的四列.
271
+
272
+ 历史行自动得到:client_category='cc'、operation=''、endpoint=''、extra_usage_json='{}'。
273
+ """
274
+ if not self._db:
275
+ return
276
+ cursor = await self._db.execute("PRAGMA table_info(usage_log)")
277
+ columns = {row["name"] for row in await cursor.fetchall()}
278
+ specs = [
279
+ ("client_category", "TEXT NOT NULL DEFAULT 'cc'"),
280
+ ("operation", "TEXT NOT NULL DEFAULT ''"),
281
+ ("endpoint", "TEXT NOT NULL DEFAULT ''"),
282
+ ("extra_usage_json", "TEXT NOT NULL DEFAULT '{}'"),
283
+ ]
284
+ for name, ddl in specs:
285
+ if name not in columns:
286
+ await self._db.execute(f"ALTER TABLE usage_log ADD COLUMN {name} {ddl}")
287
+ logger.info("Migration: added %s column to usage_log", name)
288
+
258
289
  async def _migrate_rename_backend_to_vendor(self) -> None:
259
290
  """幂等迁移:重命名 backend 列为 vendor."""
260
291
  if not self._db:
@@ -284,6 +315,10 @@ class TokenLogger:
284
315
  failover: bool = False,
285
316
  failover_from: str | None = None,
286
317
  request_id: str = "",
318
+ client_category: str = "cc",
319
+ operation: str = "",
320
+ endpoint: str = "",
321
+ extra_usage_json: str = "{}",
287
322
  ) -> None:
288
323
  if not self._db:
289
324
  return
@@ -292,8 +327,9 @@ class TokenLogger:
292
327
  (vendor, model_requested, model_served,
293
328
  input_tokens, output_tokens,
294
329
  cache_creation_tokens, cache_read_tokens,
295
- duration_ms, success, failover, failover_from, request_id)
296
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
330
+ duration_ms, success, failover, failover_from, request_id,
331
+ client_category, operation, endpoint, extra_usage_json)
332
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
297
333
  (
298
334
  vendor,
299
335
  model_requested,
@@ -307,6 +343,10 @@ class TokenLogger:
307
343
  failover,
308
344
  failover_from,
309
345
  request_id,
346
+ client_category,
347
+ operation,
348
+ endpoint,
349
+ extra_usage_json,
310
350
  ),
311
351
  )
312
352
  await self._db.commit()
@@ -376,6 +416,9 @@ class TokenLogger:
376
416
  count: int = 7,
377
417
  vendor: str | list[str] | None = None,
378
418
  model: str | list[str] | None = None,
419
+ client_category: str | list[str] | None = None,
420
+ operation: str | list[str] | None = None,
421
+ endpoint: str | list[str] | None = None,
379
422
  ) -> list[dict]:
380
423
  """按指定时间维度聚合 Token 使用统计.
381
424
 
@@ -385,6 +428,9 @@ class TokenLogger:
385
428
  ``TOTAL`` 维度下忽略此参数。
386
429
  vendor: 过滤供应商,支持单个字符串或字符串列表(多 vendor 过滤)。
387
430
  model: 过滤实际服务模型(model_served),支持单个字符串或字符串列表。
431
+ client_category: 过滤客户端类别(``'cc'`` / ``'api'``)。
432
+ operation: 过滤规范化操作名(``'chat'`` / ``'embedding'`` ...)。
433
+ endpoint: 过滤原始上游路径(``'/v1/chat/completions'`` ...)。
388
434
  """
389
435
  if not self._db:
390
436
  return []
@@ -394,6 +440,8 @@ class TokenLogger:
394
440
  sql = f"""SELECT {date_expr}, vendor,
395
441
  GROUP_CONCAT(DISTINCT model_requested) AS model_requested,
396
442
  model_served,
443
+ client_category,
444
+ operation,
397
445
  COUNT(*) AS total_requests,
398
446
  SUM(input_tokens) AS total_input,
399
447
  SUM(output_tokens) AS total_output,
@@ -420,6 +468,25 @@ class TokenLogger:
420
468
  placeholders = ",".join("?" * len(models))
421
469
  sql += f" AND model_served IN ({placeholders})"
422
470
  params.extend(models)
471
+ if client_category:
472
+ cats = (
473
+ [client_category]
474
+ if isinstance(client_category, str)
475
+ else client_category
476
+ )
477
+ placeholders = ",".join("?" * len(cats))
478
+ sql += f" AND client_category IN ({placeholders})"
479
+ params.extend(cats)
480
+ if operation:
481
+ ops = [operation] if isinstance(operation, str) else operation
482
+ placeholders = ",".join("?" * len(ops))
483
+ sql += f" AND operation IN ({placeholders})"
484
+ params.extend(ops)
485
+ if endpoint:
486
+ eps = [endpoint] if isinstance(endpoint, str) else endpoint
487
+ placeholders = ",".join("?" * len(eps))
488
+ sql += f" AND endpoint IN ({placeholders})"
489
+ params.extend(eps)
423
490
 
424
491
  sql += f" GROUP BY {group_clause} ORDER BY {order_clause}"
425
492