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.
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/PKG-INFO +1 -1
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/issue.md +54 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/pyproject.toml +1 -1
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/handler.py +331 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/operation.py +11 -7
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_handler.py +118 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/uv.lock +1 -1
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.gitignore +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/AGENTS.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/CHANGELOG.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/CLAUDE.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/LICENSE +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/README.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/convert.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/routing.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/testing.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/ci-cd.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/framework.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/user-guide.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/db.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/executor.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/routing/usage_recorder.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_banner.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_compat.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_config_init.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_currency.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_mixins.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_token.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_pricing.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_router_executor.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_schema.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_tier.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_time_range.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_types.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.4.1a1 → coding_proxy-0.4.1a2}/tests/test_vendors.py +0 -0
- {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.
|
|
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.
|
|
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(
|
|
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(?:
|
|
62
|
+
re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)generateContent/?$"),
|
|
59
63
|
"generate_content",
|
|
60
64
|
),
|
|
61
65
|
_Rule(
|
|
62
|
-
re.compile(r"^/?v1(?:
|
|
66
|
+
re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)countTokens/?$"),
|
|
63
67
|
"count_tokens",
|
|
64
68
|
),
|
|
65
69
|
_Rule(
|
|
66
|
-
re.compile(r"^/?v1(?:
|
|
70
|
+
re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)embedContent/?$"),
|
|
67
71
|
"embedding",
|
|
68
72
|
),
|
|
69
73
|
_Rule(
|
|
70
|
-
re.compile(r"^/?v1(?:
|
|
74
|
+
re.compile(r"^/?(?:v1(?:beta1?)?/)?models/[^/]+(?:%3A|:)batchEmbedContents/?$"),
|
|
71
75
|
"embedding.batch",
|
|
72
76
|
),
|
|
73
77
|
_Rule(
|
|
74
|
-
re.compile(r"^/?v1(?:
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|