coding-proxy 0.4.1a1__tar.gz → 0.4.1a3__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 (191) hide show
  1. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/.gitignore +1 -1
  2. coding_proxy-0.4.1a3/AGENTS.md +55 -0
  3. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/PKG-INFO +1 -1
  4. coding_proxy-0.4.1a3/docs/agents/knowledge-map.md +3 -0
  5. coding_proxy-0.4.1a3/docs/agents/reference-specifications.md +16 -0
  6. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/guide/monitoring.md +1 -1
  7. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/issue.md +54 -0
  8. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/user-guide.md +1 -1
  9. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/pyproject.toml +1 -1
  10. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/cli/__init__.py +1 -1
  11. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/config.default.yaml +1 -1
  12. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/server.py +1 -1
  13. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/handler.py +331 -0
  14. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/operation.py +11 -7
  15. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_native_api_handler.py +118 -0
  16. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/uv.lock +1 -1
  17. coding_proxy-0.4.1a1/AGENTS.md +0 -85
  18. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/.github/workflows/ci.yml +0 -0
  19. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/.github/workflows/coverage.yml +0 -0
  20. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/.github/workflows/release.yml +0 -0
  21. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/.pre-commit-config.yaml +0 -0
  22. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/CHANGELOG.md +0 -0
  23. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/CLAUDE.md +0 -0
  24. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/LICENSE +0 -0
  25. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/README.md +0 -0
  26. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/assets/dashboard-v0.4.0.png +0 -0
  27. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/assets/session-v0.4.0.png +0 -0
  28. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/arch/config-reference.md +0 -0
  29. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/arch/convert.md +0 -0
  30. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/arch/design-patterns.md +0 -0
  31. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/arch/routing.md +0 -0
  32. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/arch/testing.md +0 -0
  33. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/arch/vendors.md +0 -0
  34. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/ci-cd.md +0 -0
  35. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/framework.md +0 -0
  36. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/guide/api-reference.md +0 -0
  37. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/guide/cli-reference.md +0 -0
  38. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/guide/dashboard.md +0 -0
  39. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/guide/quickstart.md +0 -0
  40. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/guide/vendors.md +0 -0
  41. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/docs/zh-CN/README.md +0 -0
  42. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/__init__.py +0 -0
  43. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/__init__.py +0 -0
  44. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/__main__.py +0 -0
  45. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/__init__.py +0 -0
  46. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/providers/__init__.py +0 -0
  47. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/providers/base.py +0 -0
  48. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/providers/github.py +0 -0
  49. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/providers/google.py +0 -0
  50. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/runtime.py +0 -0
  51. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/auth/store.py +0 -0
  52. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/cli/auth_commands.py +0 -0
  53. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/cli/banner.py +0 -0
  54. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/compat/__init__.py +0 -0
  55. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/compat/canonical.py +0 -0
  56. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/compat/session_store.py +0 -0
  57. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/__init__.py +0 -0
  58. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/auth_schema.py +0 -0
  59. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/loader.py +0 -0
  60. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/resiliency.py +0 -0
  61. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/routing.py +0 -0
  62. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/schema.py +0 -0
  63. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/session_policy.py +0 -0
  64. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/config/vendors.py +0 -0
  65. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/__init__.py +0 -0
  66. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  67. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  68. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  69. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  70. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  71. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/convert/vendor_channels.py +0 -0
  72. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/logging/__init__.py +0 -0
  73. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/logging/db.py +0 -0
  74. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/logging/formatters.py +0 -0
  75. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/logging/stats.py +0 -0
  76. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/__init__.py +0 -0
  77. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/auth.py +0 -0
  78. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/compat.py +0 -0
  79. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/constants.py +0 -0
  80. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/pricing.py +0 -0
  81. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/token.py +0 -0
  82. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/model/vendor.py +0 -0
  83. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/__init__.py +0 -0
  84. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/config.py +0 -0
  85. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  86. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  87. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  88. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  89. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/routes.py +0 -0
  90. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/native_api/usage_registry.py +0 -0
  91. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/pricing.py +0 -0
  92. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/__init__.py +0 -0
  93. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  94. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/error_classifier.py +0 -0
  95. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/executor.py +0 -0
  96. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/model_mapper.py +0 -0
  97. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/quota_guard.py +0 -0
  98. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/rate_limit.py +0 -0
  99. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/retry.py +0 -0
  100. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/router.py +0 -0
  101. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/session_manager.py +0 -0
  102. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/session_policy.py +0 -0
  103. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/tier.py +0 -0
  104. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/usage_parser.py +0 -0
  105. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/routing/usage_recorder.py +0 -0
  106. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/server/__init__.py +0 -0
  107. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/server/app.py +0 -0
  108. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/server/dashboard.py +0 -0
  109. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/server/factory.py +0 -0
  110. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/server/responses.py +0 -0
  111. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/server/routes.py +0 -0
  112. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/streaming/__init__.py +0 -0
  113. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  114. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/__init__.py +0 -0
  115. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/alibaba.py +0 -0
  116. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/anthropic.py +0 -0
  117. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/antigravity.py +0 -0
  118. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/base.py +0 -0
  119. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/copilot.py +0 -0
  120. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/copilot_models.py +0 -0
  121. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  122. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  123. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/doubao.py +0 -0
  124. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/kimi.py +0 -0
  125. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/minimax.py +0 -0
  126. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/mixins.py +0 -0
  127. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  128. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/token_manager.py +0 -0
  129. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/xiaomi.py +0 -0
  130. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/src/coding/proxy/vendors/zhipu.py +0 -0
  131. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/__init__.py +0 -0
  132. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/e2e/__init__.py +0 -0
  133. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/e2e/conftest.py +0 -0
  134. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/e2e/test_e2e_http.py +0 -0
  135. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/e2e/test_e2e_token.py +0 -0
  136. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/e2e/test_e2e_vendor.py +0 -0
  137. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_antigravity.py +0 -0
  138. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_app_routes.py +0 -0
  139. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_auto_login.py +0 -0
  140. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_banner.py +0 -0
  141. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_circuit_breaker.py +0 -0
  142. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_cli_usage.py +0 -0
  143. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_compat.py +0 -0
  144. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_config_init.py +0 -0
  145. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_config_loader.py +0 -0
  146. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_convert_request.py +0 -0
  147. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_convert_response.py +0 -0
  148. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_convert_sse.py +0 -0
  149. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_copilot.py +0 -0
  150. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_copilot_convert_request.py +0 -0
  151. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_copilot_convert_response.py +0 -0
  152. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_copilot_models.py +0 -0
  153. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_copilot_urls.py +0 -0
  154. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_currency.py +0 -0
  155. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_error_classifier.py +0 -0
  156. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_logging_dual_write.py +0 -0
  157. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_mixins.py +0 -0
  158. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_auth.py +0 -0
  159. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_compat.py +0 -0
  160. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_constants.py +0 -0
  161. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_mapper.py +0 -0
  162. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_pricing.py +0 -0
  163. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_token.py +0 -0
  164. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_model_vendor.py +0 -0
  165. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_native_api_base_url_override.py +0 -0
  166. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_native_api_extractors.py +0 -0
  167. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_native_api_operation.py +0 -0
  168. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_native_api_routes.py +0 -0
  169. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_native_vendors.py +0 -0
  170. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_parse_usage.py +0 -0
  171. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_parse_usage_gemini.py +0 -0
  172. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_pricing.py +0 -0
  173. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_quota_guard.py +0 -0
  174. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_rate_limit.py +0 -0
  175. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_router_chain.py +0 -0
  176. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_router_executor.py +0 -0
  177. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_runtime_reauth.py +0 -0
  178. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_schema.py +0 -0
  179. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_session_aware.py +0 -0
  180. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_streaming_anthropic_compat.py +0 -0
  181. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_tier.py +0 -0
  182. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_tiers_config.py +0 -0
  183. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_time_range.py +0 -0
  184. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_token_logger.py +0 -0
  185. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_token_logger_native_columns.py +0 -0
  186. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_token_manager.py +0 -0
  187. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_types.py +0 -0
  188. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_vendor_channels.py +0 -0
  189. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_vendor_streaming.py +0 -0
  190. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_vendors.py +0 -0
  191. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a3}/tests/test_zhipu.py +0 -0
@@ -27,4 +27,4 @@ config.yaml
27
27
  .playwright-mcp/
28
28
 
29
29
  # Log files (dual-write logging)
30
- coding-proxy.log*
30
+ .logs/
@@ -0,0 +1,55 @@
1
+ # AGENTS.md
2
+
3
+ ## Collaboration Protocol (协作协议)
4
+
5
+ 本文件旨在规范 AI Agent(Claude Code、Antigravity 等)在本项目中的代码与文档协作行为。项目定位详见 [README.md](./README.md)。
6
+
7
+ - **Core Language**: Output MUST be in **Chinese (Simplified)** unless serving code/technical constraints.
8
+ - **Tone**: Professional, precise, and evidence-based.
9
+
10
+ ## Engineering Code of Conduct (工程行为准则)
11
+
12
+ **Core Philosophy**: **Entropy Reduction (熵减)**. 通过上下文锚定、复用驱动与标准化流水线,对抗软件系统的无序熵增。
13
+
14
+ ### 道 (Mindset - 认知心法)
15
+
16
+ - **Context-Driven (上下文驱动)**: 上下文是第一性要素 (Context Quality First)。任何变更需建立在深度理解之上(CDD),拒绝基于关键字匹配的机械式修改。
17
+ - **Minimal Intervention (最小干预)**: 遵循奥卡姆剃刀与 YAGNI 原则,仅实施必要的变更,推崇演进式设计 (Evolutionary Design) 而非过度设计。
18
+ - **Evidence-Based (循证工程)**: 杜绝主观臆断,核心决策需以**最新**且**权威**的文献(IEEE 格式)为佐证,构建“设计-实现-验证”的完整反馈闭环,确保每一项工程行动都能产生可观测的反馈信号(测试、日志、监控),以验证假设并指导迭代。
19
+ - **Systemic Integrity (系统完整性)**: 具备全局视角与二阶思维 (Second-Order Thinking),评估变更对上下游依赖及整个生态(Engine, Adapter, Agent, UI)的“涟漪效应”,不只关注变更的直接结果,更要预测“结果的结果”(如引入缓存导致的陈旧数据、重试机制引发的雪崩),优先保障整体稳定性与逻辑自洽。
20
+ - **Knowledge Crystallization (知识结晶)**: 将系统视为有机体,通过将工程错误与 AI 失败案例转化为经验约束 (Negative Prompts) 和持久化知识,驱动系统的自我进化与持续熵减。
21
+ - **Proactive Navigation (主动导航)**: 智能体不应止步于被动响应,需即时转化为“领航者”。在交付任务结果的同时,**必须**基于上下文预判并提出**下一步最佳行动建议 (Next Best Action)**,不仅交付“答案”,更要交付“路径”,消除用户决策的认知摩擦。
22
+
23
+ ### 法 (Strategy - 架构原则)
24
+
25
+ - **Plan-First Default (规划先行)**: 面对任何非琐碎任务(预估步骤 > 3 或涉及架构级决策),**必须**率先进入 Plan 模式。规划产物需明确界定:功能边界、边缘 Case 应对策略、与现有逻辑的交互锚点以及预计改动的爆炸半径。
26
+ - **Subagent Strategy (子代理并发策略)**: 面对高复杂度命题,严禁主 Agent 单点统揽。应贯彻“算力换空间”思路,果断编排 Subagent 进行任务拆解与并行攻坚,主 Agent 的职责需严格收敛于上下文协同与最终成果的组装整合。
27
+ - **Verification Before Done (交付前验证定式)**: 严禁在缺乏确凿运行证据的情况下标记任务为“已完成”。交付阶段**强制要求**提供客观自证材料:Diff 变更分析、测试用例覆盖、实施日志截图及核心链路边缘 Case 验证结果,并时刻以“方案是否能通过 Staff Engineer 严格审查”的视角自检。
28
+ - **Reuse-Driven (复用驱动)**: Compose over Reinvent。系统变更**必须**主动参考业界经典设计模式与最佳实践。在进入实质性编码前,需率先对相关领域的成熟范式进行深度调研,并结合当前项目上下文输出充分的关联分析与方案梳理。坚决贯彻“拿来主义”,优先通过组合与集成来构建系统,防范闭门造车与重复造轮子。
29
+ - **Boundary Management (边界管理)**: 严控模块/Agent 间的职责边界与契约,确保高内聚低耦合,防范隐式依赖穿透。
30
+ - **Orthogonal Decomposition (正交分解)**: 坚持“正交地提取概念主体”。识别系统中独立变化的维度并进行解耦(如机制与策略分离),确保单一概念主体的变更具备局部性,避免逻辑纠缠。
31
+ - **Single Source of Truth (单一事实源)**:严格维护唯一的权威定义源。引用时**必须**使用轻量级指针 (Link/ID) 而非数据副本 (Copy-Paste),从根源消除断裂 (Split-Brain) 风险。
32
+
33
+ ### 术 (Tactics - 执行规范)
34
+
35
+ - **Structured AI-Pair Pipeline (规范化 AI 结对流水线)**: 遵循 **Specification-Driven (规约驱动)** + **Context-Anchored (上下文锚定)** + **AI-Pair (AI 结对)** 模式,将开发固化为可审计的流水线,避免代码腐化为无法维护的“大泥球 (Big Ball of Mud)”。
36
+ - **Visual Documentation (图文并茂)**: 对于复杂逻辑,优先使用 Mermaid 图表(Sequence/Flowchart/Class)辅助说明,构建“图文并茂”的直观文档。
37
+ - **Direct Hyperlinking (直接跳转)**: 在文档中提及 Repo 内其他资源(文档/代码)时,**必须**构建可跳转的相对路径链接(如 `[Doc Name](./path.md)`),严禁使用“死文本”引用,以降低信息检索熵。
38
+ - **Operational Excellence (卓越运营)**:
39
+ 1. **Git Discipline**: 默认严禁调用 git commit;当用户显式要求提交时,一律使用 Claude Code 的自定义 Slash Command: `/commit-no-push` 进行操作(若非 Claude Code 运行环境,则读取 /commit-no-push 命令中的规则执行)。严禁执行 Rebase;
40
+ 2. **Temp Management**: 临时产物(执行计划等)一律收敛至 `.temp/` 并及时清理;
41
+ 3. **Link Validity**: 确保所有引用的 URL 可访问且具备明确的上下文价值;
42
+ 4. **Testing**: 统一在 tests/ 下维护测试用例,区分单元测试(unit)和集成测试(integration),所有测试的本地运行总时间控制在 3 min 以内;
43
+ 5. **Pre-commit Hooks**: 首次克隆仓库使用 `uv run pre-commit install` 激活本地 Git hooks,使 Ruff lint(含 auto-fix)、Ruff format 及通用代码卫生检查在每次 commit 前自动运行。若 hooks 自动修复了问题,提交会被中断,执行 `git add -p` 审阅修复内容后重新提交即可;
44
+ 6. **Issue**: 在 docs/issue.md 中维护你处理过的 Issue 摘要(问题描述、表因根因、处理方式、后续防范、同类问题影响与处理注意事项等),便于同类问题的跨上下文处理;注意识别相同 Issue,不要同 Issue 多处维护;
45
+ - **Package Management Standardization (包管理规范)**:
46
+ 1. **Python**: 严禁使用 pip/poetry,**必须**统一使用 `uv` 进行包管理与脚本执行(如 `uv run`);
47
+ 2. **JavaScript/TypeScript**: 严禁使用 npm/yarn,**必须**统一使用 `pnpm` 进行包管理与脚本执行;
48
+ - **Database Management**: 谨慎操作,数据迁移、测试等操作严禁将现有数据删除,谨慎操作数据迁移的回滚,防止数据被清理。
49
+ - **Browser Validation Protocol (浏览器验证准则)**:Agent 不得自行完成、绕过或模拟任何 OAuth / SSO 认证流程,所有登录态均来源于用户已认证的 Chrome 主 profile(真实用户登录态)。完整协议(连通性自检、凭证管理、E2E 集成、实机回归等)详见 [浏览器验证协议](./docs/agents/browser-validation.md);
50
+ 1. **安全红线**:禁止在 Sandbox 浏览器中跳转 Google 同意屏;禁止以模拟用户或第三方账号替代真实登录态;禁止要求用户在 chat 中粘贴密码、Cookie 或验证码;
51
+ - **Knowledge Map (知识索引)**:项目所有文档索引统一维护在 [知识索引](./docs/agents/knowledge-map.md),并在文档目录变更时即时同步跟新;
52
+ - **Documentation Standards (文档规范)**:采用**Mermaid Visualization Norms (Mermaid 可视化规范)**;
53
+ 1. **色彩语义与兼容性**:为图表节点配置具备语义辨识度的色彩,并确保在深色模式(Dark Mode)下具有极高的对比度与清晰度;
54
+ 2. **逻辑模块化解构**:针对业务跨度较大的架构流程,强制采用 `subgraph` 容器进行层级解构与边界划分,以增强图表的自解说(Self-explaining)能力;
55
+ - **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.4.1a1
3
+ Version: 0.4.1a3
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
@@ -0,0 +1,3 @@
1
+ # Knowledge Map(知识索引)
2
+
3
+ (WIP)
@@ -0,0 +1,16 @@
1
+ # Reference Specifications (IEEE)
2
+
3
+ > **模版准则**:[编号] 作者缩写. 姓, "文章标题," _刊名/会议名缩写 (斜体)_, 卷号, 期数, 页码, 年份.
4
+
5
+ ```latex
6
+ [1] A. Author, B. Author, and C. Author, "Title of paper," *Abbrev. Title of Journal*, vol. X, no. Y, pp. XX–XX, Year.
7
+ ```
8
+
9
+ **引用实践**
10
+
11
+ - **文内锚定**:采用标准上标链接形式:`描述内容<sup>[[1]](#ref1)</sup>`。
12
+ - **文献索引**:底层采用 HTML 锚点 `id` 实现跳转稳定性。
13
+
14
+ ```latex
15
+ <a id="ref1"></a>[1] A. Vaswani et al., "Attention is all you need," Adv. Neural Inf. Process. Syst., vol. 30, pp. 5998–6008, 2017.
16
+ ```
@@ -31,7 +31,7 @@
31
31
  ```yaml
32
32
  logging:
33
33
  level: "DEBUG" # 查看详细的模型映射和路由决策
34
- file: "coding-proxy.log" # 输出到文件
34
+ file: ".logs/coding-proxy.log" # 输出到文件
35
35
  max_bytes: 5242880 # 单文件 5 MB,触发轮转
36
36
  backup_count: 5 # 保留 5 个 gzip 压缩备份
37
37
  ```
@@ -132,3 +132,57 @@ AttributeError: 'ZhipuVendor' object has no attribute 'name'
132
132
 
133
133
  - 已 `grep -rn "vendor\.name\b" src/` 全仓扫描,确认 `target_vendor.name | vendor.name` 误用仅 routes.py 的这两处,已随本次修复一并消除。`/v1/messages` 主链路在 executor 中调用 `tier.name`(`Tier` 对象的合法 dataclass 属性),与 vendor 实例 `name` 无关,不受影响。
134
134
  - 若未来新增 Vendor 子类,仍只需实现 `get_name()` 抽象方法;外部调用方应遵循同一契约,本档案的修复模式可作为参考。
135
+
136
+ ---
137
+
138
+ ## Gemini embedding 透传至 Vertex AI 上游返回 `request body doesn't contain valid prompts`
139
+
140
+ **问题描述**
141
+
142
+ 通过本代理调用 Gemini embedding 模型时,上游返回 400:
143
+
144
+ ```
145
+ litellm.BadRequestError: GeminiException BadRequestError -
146
+ {"error":{"message":"request body doesn't contain valid prompts"}}
147
+ POST /api/gemini/v1beta/models/gemini-embedding-001%3AbatchEmbedContents 400
148
+ ```
149
+
150
+ litellm 报错日志中 URL 路径是 `:batchEmbedContents`,调用端疑似格式不兼容。
151
+
152
+ **表因**
153
+
154
+ litellm 按 Google AI Studio 格式构造请求:
155
+ - 路径:`POST {api_base}/v1beta/models/{model}:batchEmbedContents`
156
+ - Body:`{"requests": [{"model": "models/...", "content": {"parts": [{"text": "..."}]}}]}`
157
+
158
+ 但实际上游(如 `llms.as-in.io` 这类 Vertex AI 风格网关)只接受 Vertex AI 格式:
159
+ - 路径:`POST {api_base}/v1beta1/publishers/google/models/{model}:embedContent`
160
+ - Body:`{"content": {"parts": [{"text": "..."}]}}`
161
+
162
+ 且无 `batchEmbedContents` 端点。
163
+
164
+ **根因**
165
+
166
+ 1. 代理 `NativeProxyHandler.dispatch()` 是字节级透传,对 embedding 端点未做协议适配,直接把 Google AI Studio 格式的 URL/Body 转给 Vertex AI 上游,路由不匹配。
167
+ 2. litellm `_check_custom_proxy()` 在自定义 `api_base` 场景下会丢失 `v1beta/` 版本前缀,发送 `{api_base}/models/{model}:verb`,使代理原有的 `OperationClassifier` 正则(要求 `v1beta/` 前缀)失配,进而走原始透传分支再次失败。
168
+
169
+ **处理方式**
170
+
171
+ 1. `src/coding/proxy/native_api/operation.py`:放宽 Gemini 路径正则中的 `v1(?:beta1?)?/` 段为可选,兼容 litellm 丢失版本前缀的异常路径。
172
+ 2. `src/coding/proxy/native_api/handler.py`:在 `dispatch()` 中新增 Gemini embedding Vertex AI 适配分支:
173
+ - 仅当 `provider == "gemini"`、`operation in {"embedding", "embedding.batch"}`、且 `base_url` 非官方 `generativelanguage.googleapis.com` 时启用;
174
+ - `embedContent` → 重写路径为 `v1beta1/publishers/google/models/{model}:embedContent`,剥离 body 中的 `model` 字段;
175
+ - `batchEmbedContents` → 拆分为多次并发 `embedContent` 调用(`asyncio.gather`),聚合响应为 `{"embeddings": [...]}` 返回;
176
+ - 用量抽取累加各子请求的 `usageMetadata`。
177
+ 3. `tests/test_native_api_handler.py`:新增 3 个回归测试覆盖单次 / 批量 / 官方上游透传不变三类场景。
178
+
179
+ **后续防范**
180
+
181
+ - 协议适配层只对**非官方上游**生效,官方 `generativelanguage.googleapis.com` 仍走字节级透传,避免引入不必要的转换开销与协议偏差。
182
+ - 上游路径分支的判定优先用 base_url 域名而非依赖网关行为特征,便于后续扩展(如 Vertex Express、其他 LLM gateway)时的精确匹配。
183
+ - 真实链路验证:使用 litellm `embedding(api_base=..., api_key=...)` 单输入 / 多输入分别调用,确认返回 3072 维向量及正确批量计数。
184
+
185
+ **同类问题影响与处理注意事项**
186
+
187
+ - litellm 在 Gemini 其他端点(`generateContent` / `countTokens`)同样存在 `_check_custom_proxy` 丢失 `v1beta/` 前缀的 bug;本次仅放宽了 `operation.py` 中的路径正则(让分类器能识别此类异常路径),未对这些端点做格式转换,因为非 embedding 端点的 Google AI Studio / Vertex AI 请求体差异较小,多数上游兼容。如未来出现类似失配再做针对性适配。
188
+ - 若上游网关同时支持 OpenAI `/v1/embeddings` 与 Vertex AI 路径,建议优先在客户端配置 OpenAI 兼容路径,减少协议转换链路。
@@ -202,7 +202,7 @@ database:
202
202
 
203
203
  logging:
204
204
  level: "INFO" # DEBUG / INFO / WARNING / ERROR
205
- # file: "coding-proxy.log" # 输出到文件
205
+ # file: ".logs/coding-proxy.log" # 输出到文件
206
206
  # max_bytes: 5242880 # 单文件 5 MB
207
207
  # backup_count: 5 # 保留 5 个备份
208
208
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.4.1a1"
3
+ version = "0.4.1a3"
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"
@@ -109,7 +109,7 @@ def start(
109
109
  print_banner(console, host=cfg.server.host, port=cfg.server.port)
110
110
 
111
111
  # 解析文件日志路径:未显式配置时使用默认值
112
- _file_path: str | None = cfg.logging.file or "coding-proxy.log"
112
+ _file_path: str | None = cfg.logging.file or ".logs/coding-proxy.log"
113
113
  uvicorn.run(
114
114
  fastapi_app,
115
115
  host=cfg.server.host,
@@ -8,7 +8,7 @@ server:
8
8
 
9
9
  logging:
10
10
  level: "INFO"
11
- # file: "coding-proxy.log" # 文件日志路径;设为 null 或空字符串禁用
11
+ # file: ".logs/coding-proxy.log" # 文件日志路径;设为 null 或空字符串禁用
12
12
  # max_bytes: 5242880 # 单文件上限(5 MB),触发轮转
13
13
  # backup_count: 5 # 保留 gzip 压缩备份文件数
14
14
 
@@ -21,7 +21,7 @@ class LoggingConfig(BaseModel):
21
21
 
22
22
  Attributes:
23
23
  level: 控制台日志级别(INFO / WARNING / DEBUG 等)。
24
- file: 文件日志路径。为 ``None`` 时使用默认值 ``coding-proxy.log``;
24
+ file: 文件日志路径。为 ``None`` 时使用默认值 ``.logs/coding-proxy.log``;
25
25
  设为空字符串可禁用文件日志。
26
26
  max_bytes: 单个日志文件最大字节数(触发轮转)。默认 5 MB。
27
27
  backup_count: 保留的已压缩备份文件数。默认 5。
@@ -13,8 +13,10 @@
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import asyncio
16
17
  import json
17
18
  import logging
19
+ import re
18
20
  import time
19
21
  from collections.abc import AsyncIterator
20
22
  from typing import TYPE_CHECKING
@@ -194,6 +196,28 @@ class NativeProxyHandler:
194
196
  start_ts = time.perf_counter()
195
197
  client = self._get_client(provider)
196
198
 
199
+ # ── Gemini embedding Vertex AI 格式转换 ──────────────────
200
+ # 当上游非官方 Google AI Studio(generativelanguage.googleapis.com)时,
201
+ # litellm 发送的 Google AI Studio 格式(v1beta/models/{model}:batchEmbedContents)
202
+ # 需转换为 Vertex AI 格式(v1beta1/publishers/google/models/{model}:embedContent)。
203
+ vertex_rewrite = (
204
+ provider == "gemini"
205
+ and operation in ("embedding", "embedding.batch")
206
+ and cfg.base_url
207
+ and "generativelanguage.googleapis.com" not in cfg.base_url
208
+ )
209
+ if vertex_rewrite:
210
+ return await self._dispatch_gemini_vertex_embedding(
211
+ client=client,
212
+ operation=operation,
213
+ endpoint=endpoint,
214
+ body_bytes=body_bytes,
215
+ upstream_headers=upstream_headers,
216
+ query_string=query_string,
217
+ provider=provider,
218
+ start_ts=start_ts,
219
+ )
220
+
197
221
  # 构造上游 URL(保留 query)
198
222
  upstream_url = endpoint
199
223
  if query_string:
@@ -295,6 +319,313 @@ class NativeProxyHandler:
295
319
  media_type=content_type or None,
296
320
  )
297
321
 
322
+ # ── Gemini embedding → Vertex AI 格式转换 ──────────────────
323
+
324
+ # Google AI Studio 路径正则:[v1beta/]models/{model}:{verb}
325
+ # 版本段允许缺失以兼容 litellm `_check_custom_proxy` 丢失 v1beta 前缀的 bug。
326
+ _GEMINI_EMBED_PATH_RE = re.compile(
327
+ r"^/?(?:v1(?:beta1?)?/)?models/(?P<model>[^/:]+)(?::|%3A)(?P<verb>embedContent|batchEmbedContents)/?$"
328
+ )
329
+
330
+ async def _dispatch_gemini_vertex_embedding(
331
+ self,
332
+ *,
333
+ client: httpx.AsyncClient,
334
+ operation: str,
335
+ endpoint: str,
336
+ body_bytes: bytes,
337
+ upstream_headers: dict[str, str],
338
+ query_string: str,
339
+ provider: str,
340
+ start_ts: float,
341
+ ) -> StarletteResponse:
342
+ """将 Google AI Studio 格式的 embedding 请求转换为 Vertex AI 格式.
343
+
344
+ Google AI Studio:
345
+ POST v1beta/models/{model}:batchEmbedContents
346
+ Body: {"requests": [{"model": "models/{model}", "content": {...}}]}
347
+
348
+ Vertex AI:
349
+ POST v1beta1/publishers/google/models/{model}:embedContent
350
+ Body: {"content": {...}}
351
+ """
352
+ from fastapi.responses import Response as FastAPIResponse
353
+
354
+ match = self._GEMINI_EMBED_PATH_RE.match(endpoint)
355
+ if not match:
356
+ return FastAPIResponse(
357
+ content=json.dumps(
358
+ {
359
+ "error": {
360
+ "message": f"unrecognized gemini embedding path: {endpoint}"
361
+ }
362
+ }
363
+ ).encode(),
364
+ status_code=400,
365
+ media_type="application/json",
366
+ )
367
+
368
+ model_name = match.group("model")
369
+ verb = match.group("verb")
370
+
371
+ # 解析原始请求体
372
+ try:
373
+ body = json.loads(body_bytes) if body_bytes else {}
374
+ except (json.JSONDecodeError, UnicodeDecodeError):
375
+ return FastAPIResponse(
376
+ content=json.dumps(
377
+ {"error": {"message": "invalid JSON body for embedding request"}}
378
+ ).encode(),
379
+ status_code=400,
380
+ media_type="application/json",
381
+ )
382
+
383
+ if verb == "batchEmbedContents":
384
+ return await self._vertex_batch_embed(
385
+ client=client,
386
+ model_name=model_name,
387
+ body=body,
388
+ upstream_headers=upstream_headers,
389
+ query_string=query_string,
390
+ provider=provider,
391
+ operation=operation,
392
+ endpoint=endpoint,
393
+ start_ts=start_ts,
394
+ )
395
+
396
+ # 单次 embedContent:直接转换
397
+ content = body.get("content", body)
398
+ return await self._vertex_single_embed(
399
+ client=client,
400
+ model_name=model_name,
401
+ content=content,
402
+ upstream_headers=upstream_headers,
403
+ query_string=query_string,
404
+ provider=provider,
405
+ operation=operation,
406
+ endpoint=endpoint,
407
+ start_ts=start_ts,
408
+ )
409
+
410
+ async def _vertex_single_embed(
411
+ self,
412
+ *,
413
+ client: httpx.AsyncClient,
414
+ model_name: str,
415
+ content: dict,
416
+ upstream_headers: dict[str, str],
417
+ query_string: str,
418
+ provider: str,
419
+ operation: str,
420
+ endpoint: str,
421
+ start_ts: float,
422
+ ) -> StarletteResponse:
423
+ """发送单次 Vertex AI embedContent 请求."""
424
+ from fastapi.responses import Response as FastAPIResponse
425
+
426
+ vertex_path = f"/v1beta1/publishers/google/models/{model_name}:embedContent"
427
+ vertex_url = vertex_path
428
+ if query_string:
429
+ vertex_url = f"{vertex_path}?{query_string}"
430
+
431
+ vertex_body = json.dumps({"content": content}).encode()
432
+
433
+ req = client.build_request(
434
+ method="POST",
435
+ url=vertex_url,
436
+ content=vertex_body,
437
+ headers=upstream_headers,
438
+ )
439
+
440
+ try:
441
+ upstream_resp = await client.send(req, stream=True)
442
+ except (
443
+ httpx.TimeoutException,
444
+ httpx.ConnectError,
445
+ httpx.ReadError,
446
+ httpx.RemoteProtocolError,
447
+ ) as exc:
448
+ duration_ms = int((time.perf_counter() - start_ts) * 1000)
449
+ await self._record_failure(
450
+ provider=provider,
451
+ operation=operation,
452
+ endpoint=endpoint,
453
+ duration_ms=duration_ms,
454
+ reason=str(exc),
455
+ )
456
+ return FastAPIResponse(
457
+ content=json.dumps(
458
+ {
459
+ "error": {
460
+ "message": f"upstream unreachable: {exc}",
461
+ "type": "api_error",
462
+ }
463
+ }
464
+ ).encode(),
465
+ status_code=502,
466
+ media_type="application/json",
467
+ )
468
+
469
+ try:
470
+ raw_body = await upstream_resp.aread()
471
+ finally:
472
+ await upstream_resp.aclose()
473
+
474
+ duration_ms = int((time.perf_counter() - start_ts) * 1000)
475
+ status = upstream_resp.status_code
476
+ content_type = upstream_resp.headers.get("content-type", "").lower()
477
+ resp_headers = _filter_response_headers(dict(upstream_resp.headers))
478
+
479
+ # 用量抽取
480
+ extraction = ExtractionResult()
481
+ if "application/json" in content_type and raw_body:
482
+ try:
483
+ parsed = json.loads(raw_body.decode("utf-8", errors="replace"))
484
+ if isinstance(parsed, dict):
485
+ extraction = extract_usage(
486
+ provider, operation, parsed, status, dict(upstream_resp.headers)
487
+ )
488
+ except (json.JSONDecodeError, UnicodeDecodeError):
489
+ pass
490
+
491
+ vendor_label = _VENDOR_LABEL[provider]
492
+ await self._record_usage(
493
+ provider=provider,
494
+ operation=operation,
495
+ endpoint=endpoint,
496
+ duration_ms=duration_ms,
497
+ status=status,
498
+ extraction=extraction,
499
+ evidence_records=_build_nonstream_evidence(
500
+ vendor=vendor_label, extraction=extraction
501
+ ),
502
+ )
503
+
504
+ return FastAPIResponse(
505
+ content=raw_body,
506
+ status_code=status,
507
+ headers=resp_headers,
508
+ media_type=content_type or None,
509
+ )
510
+
511
+ async def _vertex_batch_embed(
512
+ self,
513
+ *,
514
+ client: httpx.AsyncClient,
515
+ model_name: str,
516
+ body: dict,
517
+ upstream_headers: dict[str, str],
518
+ query_string: str,
519
+ provider: str,
520
+ operation: str,
521
+ endpoint: str,
522
+ start_ts: float,
523
+ ) -> StarletteResponse:
524
+ """将 batchEmbedContents 拆分为多次 embedContent 调用并聚合响应."""
525
+ from fastapi.responses import Response as FastAPIResponse
526
+
527
+ requests_list = body.get("requests", [])
528
+ if not requests_list:
529
+ return FastAPIResponse(
530
+ content=json.dumps(
531
+ {
532
+ "error": {
533
+ "message": "batchEmbedContents requires non-empty 'requests' field"
534
+ }
535
+ }
536
+ ).encode(),
537
+ status_code=400,
538
+ media_type="application/json",
539
+ )
540
+
541
+ vertex_path = f"/v1beta1/publishers/google/models/{model_name}:embedContent"
542
+ vertex_url = vertex_path
543
+ if query_string:
544
+ vertex_url = f"{vertex_path}?{query_string}"
545
+
546
+ # 并发发送所有 embedContent 请求
547
+ async def _single(req_body: dict) -> tuple[dict, int]:
548
+ content = req_body.get("content", req_body)
549
+ vertex_body = json.dumps({"content": content}).encode()
550
+ req = client.build_request(
551
+ method="POST",
552
+ url=vertex_url,
553
+ content=vertex_body,
554
+ headers=upstream_headers,
555
+ )
556
+ try:
557
+ resp = await client.send(req, stream=False)
558
+ except (
559
+ httpx.TimeoutException,
560
+ httpx.ConnectError,
561
+ httpx.ReadError,
562
+ httpx.RemoteProtocolError,
563
+ ) as exc:
564
+ return {"error": {"message": f"upstream unreachable: {exc}"}}, 502
565
+ try:
566
+ return resp.json(), resp.status_code
567
+ except Exception:
568
+ return {"error": {"message": resp.text[:200]}}, resp.status_code
569
+
570
+ results = await asyncio.gather(*[_single(r) for r in requests_list])
571
+
572
+ # 检查是否有失败的请求
573
+ embeddings = []
574
+ for resp_json, resp_status in results:
575
+ if resp_status != 200:
576
+ # 返回第一个错误
577
+ return FastAPIResponse(
578
+ content=json.dumps(resp_json).encode(),
579
+ status_code=resp_status,
580
+ media_type="application/json",
581
+ )
582
+ embedding_data = resp_json.get("embedding", {})
583
+ embeddings.append(embedding_data)
584
+
585
+ # 聚合为 batchEmbedContents 响应格式
586
+ batch_response = {"embeddings": embeddings}
587
+ duration_ms = int((time.perf_counter() - start_ts) * 1000)
588
+
589
+ # 用量抽取
590
+ extraction = ExtractionResult()
591
+ for resp_json, _ in results:
592
+ if isinstance(resp_json, dict):
593
+ ext = extract_usage(provider, operation, resp_json, 200, {})
594
+ extraction = ExtractionResult(
595
+ input_tokens=extraction.input_tokens + ext.input_tokens,
596
+ output_tokens=extraction.output_tokens + ext.output_tokens,
597
+ cache_creation_tokens=extraction.cache_creation_tokens
598
+ + ext.cache_creation_tokens,
599
+ cache_read_tokens=extraction.cache_read_tokens
600
+ + ext.cache_read_tokens,
601
+ request_id=ext.request_id or extraction.request_id,
602
+ model_served=ext.model_served or extraction.model_served,
603
+ raw_usage=ext.raw_usage or extraction.raw_usage,
604
+ source_field_map=ext.source_field_map
605
+ or extraction.source_field_map,
606
+ evidence_kind=ext.evidence_kind or extraction.evidence_kind,
607
+ extra_usage=ext.extra_usage or extraction.extra_usage,
608
+ )
609
+
610
+ vendor_label = _VENDOR_LABEL[provider]
611
+ await self._record_usage(
612
+ provider=provider,
613
+ operation=operation,
614
+ endpoint=endpoint,
615
+ duration_ms=duration_ms,
616
+ status=200,
617
+ extraction=extraction,
618
+ evidence_records=_build_nonstream_evidence(
619
+ vendor=vendor_label, extraction=extraction
620
+ ),
621
+ )
622
+
623
+ return FastAPIResponse(
624
+ content=json.dumps(batch_response).encode(),
625
+ status_code=200,
626
+ media_type="application/json",
627
+ )
628
+
298
629
  # ── SSE 流式转发(同时累加 usage) ─────────────────────────
299
630
 
300
631
  async def _stream_and_accumulate(
@@ -48,30 +48,34 @@ _OPENAI_RULES: tuple[_Rule, ...] = (
48
48
  )
49
49
 
50
50
  # ── Gemini ────────────────────────────────────────────────────────
51
- # Gemini 的方法动词作为路径后缀(``:generateContent``),通过正则提取
51
+ # Gemini 的方法动词作为路径后缀(``:generateContent``),通过正则提取。
52
+ # ``v1(?:beta1?)?/`` 前缀允许缺失,以兼容 litellm `_check_custom_proxy` 在
53
+ # 自定义 ``api_base`` 场景下丢失版本段的 bug(参考 litellm issue #17759)。
52
54
  _GEMINI_RULES: tuple[_Rule, ...] = (
53
55
  _Rule(
54
- re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)streamGenerateContent/?$"),
56
+ re.compile(
57
+ r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)streamGenerateContent/?$"
58
+ ),
55
59
  "generate_content",
56
60
  ),
57
61
  _Rule(
58
- re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)generateContent/?$"),
62
+ re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)generateContent/?$"),
59
63
  "generate_content",
60
64
  ),
61
65
  _Rule(
62
- re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)countTokens/?$"),
66
+ re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)countTokens/?$"),
63
67
  "count_tokens",
64
68
  ),
65
69
  _Rule(
66
- re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)embedContent/?$"),
70
+ re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)embedContent/?$"),
67
71
  "embedding",
68
72
  ),
69
73
  _Rule(
70
- re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)batchEmbedContents/?$"),
74
+ re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)batchEmbedContents/?$"),
71
75
  "embedding.batch",
72
76
  ),
73
77
  _Rule(
74
- re.compile(r"^/?v1(?:beta)?/models/[^/]+(?:%3A|:)predict/?$"),
78
+ re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)predict/?$"),
75
79
  "predict",
76
80
  ),
77
81
  _Rule(