coding-proxy 0.4.1a1__tar.gz → 0.4.1a2__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 (188) hide show
  1. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/PKG-INFO +1 -1
  2. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/issue.md +54 -0
  3. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/pyproject.toml +1 -1
  4. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/handler.py +331 -0
  5. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/operation.py +11 -7
  6. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_handler.py +118 -0
  7. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/uv.lock +1 -1
  8. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.github/workflows/ci.yml +0 -0
  9. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.github/workflows/coverage.yml +0 -0
  10. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.github/workflows/release.yml +0 -0
  11. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.gitignore +0 -0
  12. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.pre-commit-config.yaml +0 -0
  13. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/AGENTS.md +0 -0
  14. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/CHANGELOG.md +0 -0
  15. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/CLAUDE.md +0 -0
  16. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/LICENSE +0 -0
  17. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/README.md +0 -0
  18. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/assets/dashboard-v0.4.0.png +0 -0
  19. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/assets/session-v0.4.0.png +0 -0
  20. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/config-reference.md +0 -0
  21. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/convert.md +0 -0
  22. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/design-patterns.md +0 -0
  23. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/routing.md +0 -0
  24. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/testing.md +0 -0
  25. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/vendors.md +0 -0
  26. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/ci-cd.md +0 -0
  27. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/framework.md +0 -0
  28. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/api-reference.md +0 -0
  29. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/cli-reference.md +0 -0
  30. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/dashboard.md +0 -0
  31. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/monitoring.md +0 -0
  32. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/quickstart.md +0 -0
  33. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/vendors.md +0 -0
  34. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/user-guide.md +0 -0
  35. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/zh-CN/README.md +0 -0
  36. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/__init__.py +0 -0
  37. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/__init__.py +0 -0
  38. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/__main__.py +0 -0
  39. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/__init__.py +0 -0
  40. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/__init__.py +0 -0
  41. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/base.py +0 -0
  42. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/github.py +0 -0
  43. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/google.py +0 -0
  44. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/runtime.py +0 -0
  45. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/store.py +0 -0
  46. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/cli/__init__.py +0 -0
  47. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/cli/auth_commands.py +0 -0
  48. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/cli/banner.py +0 -0
  49. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/compat/__init__.py +0 -0
  50. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/compat/canonical.py +0 -0
  51. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/compat/session_store.py +0 -0
  52. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/__init__.py +0 -0
  53. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/auth_schema.py +0 -0
  54. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/config.default.yaml +0 -0
  55. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/loader.py +0 -0
  56. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/resiliency.py +0 -0
  57. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/routing.py +0 -0
  58. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/schema.py +0 -0
  59. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/server.py +0 -0
  60. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/session_policy.py +0 -0
  61. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/vendors.py +0 -0
  62. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/__init__.py +0 -0
  63. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  64. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  65. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  66. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  67. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  68. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/vendor_channels.py +0 -0
  69. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/__init__.py +0 -0
  70. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/db.py +0 -0
  71. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/formatters.py +0 -0
  72. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/stats.py +0 -0
  73. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/__init__.py +0 -0
  74. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/auth.py +0 -0
  75. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/compat.py +0 -0
  76. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/constants.py +0 -0
  77. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/pricing.py +0 -0
  78. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/token.py +0 -0
  79. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/vendor.py +0 -0
  80. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/__init__.py +0 -0
  81. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/config.py +0 -0
  82. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
  83. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
  84. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
  85. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/openai.py +0 -0
  86. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/routes.py +0 -0
  87. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/usage_registry.py +0 -0
  88. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/pricing.py +0 -0
  89. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/__init__.py +0 -0
  90. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  91. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/error_classifier.py +0 -0
  92. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/executor.py +0 -0
  93. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/model_mapper.py +0 -0
  94. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/quota_guard.py +0 -0
  95. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/rate_limit.py +0 -0
  96. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/retry.py +0 -0
  97. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/router.py +0 -0
  98. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/session_manager.py +0 -0
  99. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/session_policy.py +0 -0
  100. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/tier.py +0 -0
  101. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/usage_parser.py +0 -0
  102. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/usage_recorder.py +0 -0
  103. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/__init__.py +0 -0
  104. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/app.py +0 -0
  105. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/dashboard.py +0 -0
  106. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/factory.py +0 -0
  107. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/responses.py +0 -0
  108. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/routes.py +0 -0
  109. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/streaming/__init__.py +0 -0
  110. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  111. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/__init__.py +0 -0
  112. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/alibaba.py +0 -0
  113. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/anthropic.py +0 -0
  114. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/antigravity.py +0 -0
  115. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/base.py +0 -0
  116. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot.py +0 -0
  117. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot_models.py +0 -0
  118. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  119. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  120. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/doubao.py +0 -0
  121. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/kimi.py +0 -0
  122. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/minimax.py +0 -0
  123. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/mixins.py +0 -0
  124. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  125. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/token_manager.py +0 -0
  126. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/xiaomi.py +0 -0
  127. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/zhipu.py +0 -0
  128. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/__init__.py +0 -0
  129. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/__init__.py +0 -0
  130. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/conftest.py +0 -0
  131. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/test_e2e_http.py +0 -0
  132. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/test_e2e_token.py +0 -0
  133. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/test_e2e_vendor.py +0 -0
  134. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_antigravity.py +0 -0
  135. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_app_routes.py +0 -0
  136. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_auto_login.py +0 -0
  137. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_banner.py +0 -0
  138. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_circuit_breaker.py +0 -0
  139. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_cli_usage.py +0 -0
  140. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_compat.py +0 -0
  141. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_config_init.py +0 -0
  142. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_config_loader.py +0 -0
  143. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_convert_request.py +0 -0
  144. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_convert_response.py +0 -0
  145. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_convert_sse.py +0 -0
  146. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot.py +0 -0
  147. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_convert_request.py +0 -0
  148. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_convert_response.py +0 -0
  149. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_models.py +0 -0
  150. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_urls.py +0 -0
  151. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_currency.py +0 -0
  152. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_error_classifier.py +0 -0
  153. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_logging_dual_write.py +0 -0
  154. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_mixins.py +0 -0
  155. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_auth.py +0 -0
  156. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_compat.py +0 -0
  157. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_constants.py +0 -0
  158. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_mapper.py +0 -0
  159. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_pricing.py +0 -0
  160. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_token.py +0 -0
  161. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_vendor.py +0 -0
  162. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_base_url_override.py +0 -0
  163. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_extractors.py +0 -0
  164. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_operation.py +0 -0
  165. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_routes.py +0 -0
  166. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_vendors.py +0 -0
  167. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_parse_usage.py +0 -0
  168. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_parse_usage_gemini.py +0 -0
  169. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_pricing.py +0 -0
  170. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_quota_guard.py +0 -0
  171. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_rate_limit.py +0 -0
  172. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_router_chain.py +0 -0
  173. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_router_executor.py +0 -0
  174. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_runtime_reauth.py +0 -0
  175. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_schema.py +0 -0
  176. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_session_aware.py +0 -0
  177. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_streaming_anthropic_compat.py +0 -0
  178. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_tier.py +0 -0
  179. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_tiers_config.py +0 -0
  180. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_time_range.py +0 -0
  181. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_token_logger.py +0 -0
  182. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_token_logger_native_columns.py +0 -0
  183. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_token_manager.py +0 -0
  184. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_types.py +0 -0
  185. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_vendor_channels.py +0 -0
  186. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_vendor_streaming.py +0 -0
  187. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_vendors.py +0 -0
  188. {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_zhipu.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.4.1a1
3
+ Version: 0.4.1a2
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
@@ -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 兼容路径,减少协议转换链路。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.4.1a1"
3
+ version = "0.4.1a2"
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"
@@ -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(
@@ -14,6 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import json
17
18
  from collections.abc import Iterator
18
19
 
19
20
  import httpx
@@ -443,3 +444,120 @@ def test_gemini_url_encoded_colon_decoded_for_upstream() -> None:
443
444
  # 上游 URL 必须含字面冒号,不含 %3A
444
445
  assert "%3A" not in upstream_str
445
446
  assert ":batchEmbedContents" in upstream_str
447
+
448
+
449
+ # ── Gemini embedding Vertex AI 格式转换 ─────────────────────────
450
+
451
+
452
+ def test_gemini_vertex_embed_content_single() -> None:
453
+ """非官方上游时,embedContent 转为 Vertex AI 格式."""
454
+
455
+ def route(request: httpx.Request) -> httpx.Response:
456
+ body = json.loads(request.content)
457
+ assert "content" in body
458
+ assert "model" not in body
459
+ assert "requests" not in body
460
+ assert ":embedContent" in str(request.url)
461
+ assert "v1beta1/publishers/google/models" in str(request.url)
462
+ return httpx.Response(200, json={"embedding": {"values": [0.1, 0.2]}})
463
+
464
+ def factory(make_transport):
465
+ cfg = NativeApiConfig(
466
+ gemini=NativeProviderConfig(enabled=True, base_url="http://llms.as-in.io"),
467
+ )
468
+ transport = make_transport(route)
469
+ return NativeProxyHandler(cfg, transport=transport), transport
470
+
471
+ for client, captured in _make_app(factory):
472
+ r = client.post(
473
+ "/api/gemini/v1beta/models/gemini-embedding-2-preview:embedContent",
474
+ json={
475
+ "model": "models/gemini-embedding-2-preview",
476
+ "content": {"parts": [{"text": "hello"}]},
477
+ },
478
+ )
479
+ assert r.status_code == 200
480
+ assert "embedding" in r.json()
481
+
482
+
483
+ def test_gemini_vertex_batch_embed_contents() -> None:
484
+ """非官方上游时,batchEmbedContents 拆分为多次 embedContent 并聚合."""
485
+
486
+ call_count = 0
487
+
488
+ def route(request: httpx.Request) -> httpx.Response:
489
+ nonlocal call_count
490
+ call_count += 1
491
+ body = json.loads(request.content)
492
+ assert "content" in body
493
+ assert ":embedContent" in str(request.url)
494
+ assert "v1beta1/publishers/google/models" in str(request.url)
495
+ return httpx.Response(
496
+ 200,
497
+ json={"embedding": {"values": [float(call_count), 0.5]}},
498
+ )
499
+
500
+ def factory(make_transport):
501
+ cfg = NativeApiConfig(
502
+ gemini=NativeProviderConfig(enabled=True, base_url="http://llms.as-in.io"),
503
+ )
504
+ transport = make_transport(route)
505
+ return NativeProxyHandler(cfg, transport=transport), transport
506
+
507
+ for client, captured in _make_app(factory):
508
+ r = client.post(
509
+ "/api/gemini/v1beta/models/gemini-embedding-2-preview:batchEmbedContents",
510
+ json={
511
+ "requests": [
512
+ {
513
+ "model": "models/gemini-embedding-2-preview",
514
+ "content": {"parts": [{"text": "hello"}]},
515
+ },
516
+ {
517
+ "model": "models/gemini-embedding-2-preview",
518
+ "content": {"parts": [{"text": "world"}]},
519
+ },
520
+ ]
521
+ },
522
+ )
523
+ assert r.status_code == 200
524
+ data = r.json()
525
+ assert "embeddings" in data
526
+ assert len(data["embeddings"]) == 2
527
+ assert data["embeddings"][0]["values"] == [1.0, 0.5]
528
+ assert data["embeddings"][1]["values"] == [2.0, 0.5]
529
+ assert call_count == 2
530
+
531
+
532
+ def test_gemini_vertex_embed_official_upstream_unchanged() -> None:
533
+ """官方上游时,batchEmbedContents 走原始透传路径,不做格式转换."""
534
+
535
+ def route(request: httpx.Request) -> httpx.Response:
536
+ return httpx.Response(200, json={"embeddings": [{"values": [0.1, 0.2]}]})
537
+
538
+ def factory(make_transport):
539
+ cfg = NativeApiConfig(
540
+ gemini=NativeProviderConfig(
541
+ enabled=True, base_url="https://generativelanguage.googleapis.com"
542
+ ),
543
+ )
544
+ transport = make_transport(route)
545
+ return NativeProxyHandler(cfg, transport=transport), transport
546
+
547
+ for client, captured in _make_app(factory):
548
+ r = client.post(
549
+ "/api/gemini/v1beta/models/gemini-embedding-001:batchEmbedContents?key=k",
550
+ json={
551
+ "requests": [
552
+ {
553
+ "model": "models/gemini-embedding-001",
554
+ "content": {"parts": [{"text": "hello"}]},
555
+ }
556
+ ]
557
+ },
558
+ )
559
+ assert r.status_code == 200
560
+ # 官方上游走原始路径,URL 保持 v1beta/models/ 格式
561
+ upstream = captured[0]
562
+ assert "v1beta/models" in str(upstream.url)
563
+ assert "v1beta1/publishers" not in str(upstream.url)
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "coding-proxy"
77
- version = "0.4.1a1"
77
+ version = "0.4.1a2"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes