coding-proxy 0.5.1a4__tar.gz → 0.5.1a5__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.5.1a4 → coding_proxy-0.5.1a5}/PKG-INFO +1 -1
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/pyproject.toml +1 -1
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/logging/db.py +14 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/executor.py +84 -8
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/usage_recorder.py +5 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_router_executor.py +288 -4
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/uv.lock +1 -1
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/.github/workflows/ci.yml +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/.github/workflows/coverage.yml +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/.gitignore +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/.pre-commit-config.yaml +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/AGENTS.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/CHANGELOG.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/CLAUDE.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/LICENSE +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/README.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/assets/dashboard-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/assets/model-calling-v0.5.0.png +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/assets/session-v0.4.0.png +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/.agents/browser-validation.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/.agents/issue.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/.agents/knowledge-map.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/.agents/reference-specifications.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/arch/config-reference.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/arch/convert.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/arch/design-patterns.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/arch/routing.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/arch/testing.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/arch/vendors.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/framework.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/guide/api-reference.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/guide/cli-reference.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/guide/dashboard.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/guide/monitoring.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/guide/quickstart.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/guide/vendors.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/ops/ci-cd.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/user-guide.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/docs/zh-CN/README.md +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/providers/github.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/providers/google.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/runtime.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/auth/store.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/cli/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/cli/auth_commands.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/cli/banner.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/compat/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/compat/canonical.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/compat/session_store.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/auth_schema.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/config.default.yaml +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/loader.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/resiliency.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/routing.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/schema.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/server.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/session_policy.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/config/vendors.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/convert/vendor_channels.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/logging/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/logging/formatters.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/logging/stats.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/compat.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/constants.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/pricing.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/token.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/model/vendor.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/config.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/extractors/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/extractors/anthropic.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/extractors/gemini.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/extractors/openai.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/handler.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/operation.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/routes.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/native_api/usage_registry.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/error_classifier.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/model_mapper.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/quota_guard.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/rate_limit.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/retry.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/router.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/session_manager.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/session_policy.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/tier.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/routing/usage_parser.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/server/app.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/server/dashboard.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/server/factory.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/server/responses.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/server/routes.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/alibaba.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/anthropic.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/antigravity.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/base.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/concurrency.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/copilot.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/copilot_models.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/copilot_urls.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/doubao.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/kimi.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/minimax.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/native_anthropic.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/token_manager.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/xiaomi.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/src/coding/proxy/vendors/zhipu.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/e2e/__init__.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/e2e/conftest.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/e2e/test_e2e_http.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/e2e/test_e2e_token.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/e2e/test_e2e_vendor.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_antigravity.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_app_routes.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_auto_login.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_banner.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_cli_usage.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_compat.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_concurrency_monitor.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_config_init.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_config_loader.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_convert_request.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_convert_response.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_convert_sse.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_copilot.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_copilot_convert_request.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_copilot_convert_response.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_copilot_models.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_copilot_urls.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_currency.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_error_classifier.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_executor_in_flight_tracking.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_logging_dual_write.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_mixins.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_auth.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_compat.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_constants.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_mapper.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_pricing.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_token.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_model_vendor.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_native_api_base_url_override.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_native_api_extractors.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_native_api_handler.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_native_api_operation.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_native_api_routes.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_native_vendors.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_parse_usage.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_parse_usage_gemini.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_pricing.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_quota_guard.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_rate_limit.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_router_chain.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_runtime_reauth.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_schema.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_session_aware.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_streaming_anthropic_compat.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_tier.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_tiers_config.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_time_range.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_token_logger.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_token_logger_native_columns.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_token_manager.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_types.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_vendor_channels.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_vendor_streaming.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_vendors.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_zhipu.py +0 -0
- {coding_proxy-0.5.1a4 → coding_proxy-0.5.1a5}/tests/test_zhipu_concurrency.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1a5
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
6
|
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "coding-proxy"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1a5"
|
|
4
4
|
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao..."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -335,6 +335,20 @@ class TokenLogger:
|
|
|
335
335
|
)
|
|
336
336
|
await self._db.commit()
|
|
337
337
|
|
|
338
|
+
async def update_empty_session_title(self, session_key: str, title: str) -> None:
|
|
339
|
+
"""为标题为空的 session 补写标题(幂等,仅覆盖空标题行).
|
|
340
|
+
|
|
341
|
+
使用 ``AND title = ''`` 条件确保不覆盖已有标题,
|
|
342
|
+
即使行已存在但标题为空也会被更新。
|
|
343
|
+
"""
|
|
344
|
+
if not self._db or not title or not session_key:
|
|
345
|
+
return
|
|
346
|
+
await self._db.execute(
|
|
347
|
+
"UPDATE session_meta SET title = ? WHERE session_key = ? AND title = ''",
|
|
348
|
+
(title, session_key),
|
|
349
|
+
)
|
|
350
|
+
await self._db.commit()
|
|
351
|
+
|
|
338
352
|
async def get_session_titles(self, session_keys: list[str]) -> dict[str, str]:
|
|
339
353
|
"""批量查询 session 标题."""
|
|
340
354
|
if not self._db or not session_keys:
|
|
@@ -50,11 +50,13 @@ from ..compat.canonical import (
|
|
|
50
50
|
CompatibilityStatus,
|
|
51
51
|
build_canonical_request,
|
|
52
52
|
)
|
|
53
|
-
from ..model.compat import CanonicalRequest
|
|
53
|
+
from ..model.compat import CanonicalMessagePart, CanonicalRequest
|
|
54
54
|
|
|
55
55
|
logger = logging.getLogger(__name__)
|
|
56
56
|
|
|
57
57
|
_SESSION_TITLE_MAX_LEN = 600
|
|
58
|
+
# 回退标题截取长度 — 工具结果等非用户直接输入的摘要上限。
|
|
59
|
+
_FALLBACK_TITLE_MAX_LEN = 80
|
|
58
60
|
|
|
59
61
|
# Claude Code 注入的"噪声"标签 — 系统级上下文,不应进入 Session 标题。
|
|
60
62
|
# 这些标签由 CC harness 在首个 user 消息 content 中拼接,高度同质,
|
|
@@ -63,7 +65,8 @@ _NOISE_TAG_PATTERN = re.compile(
|
|
|
63
65
|
r"<(?P<tag>system-reminder|user-preferences|"
|
|
64
66
|
r"local-command-stdout|local-command-stderr|"
|
|
65
67
|
r"bash-input|bash-stdout|bash-stderr|"
|
|
66
|
-
r"ide_selection|stdin|system_instruction|session
|
|
68
|
+
r"ide_selection|stdin|system_instruction|session|"
|
|
69
|
+
r"artifactMetadata|thinking)\b[^>]*>"
|
|
67
70
|
r".*?</(?P=tag)>",
|
|
68
71
|
flags=re.DOTALL | re.IGNORECASE,
|
|
69
72
|
)
|
|
@@ -109,13 +112,19 @@ def _sanitize_user_text(raw: str) -> str:
|
|
|
109
112
|
return re.sub(r"\s+", " ", cleaned).strip()
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
# ── Session 标题提取: 多层级回退策略 ──────────────────────────────
|
|
116
|
+
#
|
|
117
|
+
# Level 1: user TEXT → 噪声剥离 → 首条非空文本 (原有逻辑)
|
|
118
|
+
# Level 2: user TOOL_RESULT → text 截取 → "[Tool output] <snippet>"
|
|
119
|
+
# Level 3: user IMAGE → 计数 → "[1 Image]" / "[N Images]"
|
|
120
|
+
# Level 4: 请求元数据 → tool_names / model → "[Tool call] Bash, Read"
|
|
121
|
+
# / "[Session] claude-opus-4-8"
|
|
122
|
+
# ─────────────────────────────────────────────────────────────────
|
|
114
123
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"""
|
|
118
|
-
for part in
|
|
124
|
+
|
|
125
|
+
def _extract_title_from_user_text(messages: list[CanonicalMessagePart]) -> str:
|
|
126
|
+
"""Level 1: 从 user TEXT 部分提取经噪声剥离后的首条非空文本."""
|
|
127
|
+
for part in messages:
|
|
119
128
|
if part.role != "user" or part.type != CanonicalPartType.TEXT:
|
|
120
129
|
continue
|
|
121
130
|
cleaned = _sanitize_user_text(part.text)
|
|
@@ -124,6 +133,59 @@ def _extract_session_title(request: CanonicalRequest) -> str:
|
|
|
124
133
|
return ""
|
|
125
134
|
|
|
126
135
|
|
|
136
|
+
def _extract_title_from_tool_results(messages: list[CanonicalMessagePart]) -> str:
|
|
137
|
+
"""Level 2: 从 user TOOL_RESULT 部分截取文本摘要."""
|
|
138
|
+
for part in messages:
|
|
139
|
+
if part.role != "user" or part.type != CanonicalPartType.TOOL_RESULT:
|
|
140
|
+
continue
|
|
141
|
+
if not part.text:
|
|
142
|
+
continue
|
|
143
|
+
cleaned = _sanitize_user_text(part.text)
|
|
144
|
+
if cleaned:
|
|
145
|
+
snippet = cleaned[:_FALLBACK_TITLE_MAX_LEN]
|
|
146
|
+
return f"[Tool output] {snippet}"
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _extract_title_from_images(messages: list[CanonicalMessagePart]) -> str:
|
|
151
|
+
"""Level 3: 统计 user IMAGE 部分数量,生成图片描述标题."""
|
|
152
|
+
count = sum(
|
|
153
|
+
1 for p in messages if p.role == "user" and p.type == CanonicalPartType.IMAGE
|
|
154
|
+
)
|
|
155
|
+
if count == 0:
|
|
156
|
+
return ""
|
|
157
|
+
return f"[{count} Image{'s' if count > 1 else ''}]"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _extract_title_from_metadata(request: CanonicalRequest) -> str:
|
|
161
|
+
"""Level 4: 从请求元数据 (tool_names / model) 合成兜底标题."""
|
|
162
|
+
if request.tool_names:
|
|
163
|
+
names = ", ".join(request.tool_names[:3])
|
|
164
|
+
return f"[Tool call] {names}"
|
|
165
|
+
if request.model:
|
|
166
|
+
return f"[Session] {request.model}"
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _extract_session_title(request: CanonicalRequest) -> str:
|
|
171
|
+
"""从规范化请求中提取 session 标题 — 多层级回退策略。
|
|
172
|
+
|
|
173
|
+
依次尝试: user TEXT 噪声剥离 → TOOL_RESULT 摘要 → IMAGE 计数 → 元数据兜底。
|
|
174
|
+
任意级别命中即返回,确保 Dashboard 尽可能展示有辨识度的标题。
|
|
175
|
+
"""
|
|
176
|
+
messages = request.messages
|
|
177
|
+
for extractor in (
|
|
178
|
+
_extract_title_from_user_text,
|
|
179
|
+
_extract_title_from_tool_results,
|
|
180
|
+
_extract_title_from_images,
|
|
181
|
+
):
|
|
182
|
+
title = extractor(messages)
|
|
183
|
+
if title:
|
|
184
|
+
return title[:_SESSION_TITLE_MAX_LEN]
|
|
185
|
+
# Level 4 依赖 request 元数据,签名不同
|
|
186
|
+
return _extract_title_from_metadata(request)[:_SESSION_TITLE_MAX_LEN]
|
|
187
|
+
|
|
188
|
+
|
|
127
189
|
def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
|
|
128
190
|
"""构建语义拒绝的请求体诊断上下文.
|
|
129
191
|
|
|
@@ -663,6 +725,13 @@ class _RouteExecutor:
|
|
|
663
725
|
await self._recorder.set_session_title(
|
|
664
726
|
canonical_request.session_key, title
|
|
665
727
|
)
|
|
728
|
+
else:
|
|
729
|
+
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
|
|
730
|
+
title = _extract_session_title(canonical_request)
|
|
731
|
+
if title:
|
|
732
|
+
await self._recorder.update_empty_session_title(
|
|
733
|
+
canonical_request.session_key, title
|
|
734
|
+
)
|
|
666
735
|
incompatible_reasons: list[str] = []
|
|
667
736
|
effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
|
|
668
737
|
last_idx = len(effective_tiers) - 1
|
|
@@ -842,6 +911,13 @@ class _RouteExecutor:
|
|
|
842
911
|
await self._recorder.set_session_title(
|
|
843
912
|
canonical_request.session_key, title
|
|
844
913
|
)
|
|
914
|
+
else:
|
|
915
|
+
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
|
|
916
|
+
title = _extract_session_title(canonical_request)
|
|
917
|
+
if title:
|
|
918
|
+
await self._recorder.update_empty_session_title(
|
|
919
|
+
canonical_request.session_key, title
|
|
920
|
+
)
|
|
845
921
|
incompatible_reasons: list[str] = []
|
|
846
922
|
effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
|
|
847
923
|
last_idx = len(effective_tiers) - 1
|
|
@@ -33,6 +33,11 @@ class UsageRecorder:
|
|
|
33
33
|
if self._token_logger:
|
|
34
34
|
await self._token_logger.set_session_title(session_key, title)
|
|
35
35
|
|
|
36
|
+
async def update_empty_session_title(self, session_key: str, title: str) -> None:
|
|
37
|
+
"""为标题为空的 session 补写标题(委托给 TokenLogger)."""
|
|
38
|
+
if self._token_logger:
|
|
39
|
+
await self._token_logger.update_empty_session_title(session_key, title)
|
|
40
|
+
|
|
36
41
|
# ── 用量信息构建 ──────────────────────────────────────
|
|
37
42
|
|
|
38
43
|
@staticmethod
|
|
@@ -20,10 +20,15 @@ from coding.proxy.compat.canonical import (
|
|
|
20
20
|
build_canonical_request,
|
|
21
21
|
)
|
|
22
22
|
from coding.proxy.routing.executor import (
|
|
23
|
+
_FALLBACK_TITLE_MAX_LEN,
|
|
23
24
|
_SESSION_TITLE_MAX_LEN,
|
|
24
25
|
_VENDOR_PROTOCOL_LABEL_MAP,
|
|
25
26
|
_build_semantic_rejection_diagnostic,
|
|
26
27
|
_extract_session_title,
|
|
28
|
+
_extract_title_from_images,
|
|
29
|
+
_extract_title_from_metadata,
|
|
30
|
+
_extract_title_from_tool_results,
|
|
31
|
+
_extract_title_from_user_text,
|
|
27
32
|
_has_tool_results,
|
|
28
33
|
_is_likely_request_format_error,
|
|
29
34
|
_log_vendor_response_error,
|
|
@@ -2258,6 +2263,20 @@ class TestSanitizeUserText:
|
|
|
2258
2263
|
raw = "<session>\nline1\nline2\n</session>真实标题"
|
|
2259
2264
|
assert _sanitize_user_text(raw) == "真实标题"
|
|
2260
2265
|
|
|
2266
|
+
def test_strips_artifact_metadata_tag(self):
|
|
2267
|
+
"""``<artifactMetadata>`` 标签应被完整剥离."""
|
|
2268
|
+
raw = "<artifactMetadata>artifact context</artifactMetadata>用户文本"
|
|
2269
|
+
assert _sanitize_user_text(raw) == "用户文本"
|
|
2270
|
+
|
|
2271
|
+
def test_strips_thinking_tag(self):
|
|
2272
|
+
"""``<thinking>`` 标签应被完整剥离."""
|
|
2273
|
+
raw = "<thinking>内部推理过程</thinking>用户实际提问"
|
|
2274
|
+
assert _sanitize_user_text(raw) == "用户实际提问"
|
|
2275
|
+
|
|
2276
|
+
def test_strips_thinking_tag_multiline(self):
|
|
2277
|
+
raw = "<thinking>\nline1\nline2\n</thinking>清理后文本"
|
|
2278
|
+
assert _sanitize_user_text(raw) == "清理后文本"
|
|
2279
|
+
|
|
2261
2280
|
|
|
2262
2281
|
class TestExtractSessionTitle:
|
|
2263
2282
|
"""``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题."""
|
|
@@ -2304,14 +2323,16 @@ class TestExtractSessionTitle:
|
|
|
2304
2323
|
req = self._build_request([{"role": "user", "content": raw}])
|
|
2305
2324
|
assert _extract_session_title(req) == "/commit feat: 新增标题清洗"
|
|
2306
2325
|
|
|
2307
|
-
def
|
|
2326
|
+
def test_returns_metadata_fallback_when_only_noise(self):
|
|
2327
|
+
"""纯噪声文本回退到 Level 4 元数据兜底(使用 model 名称)."""
|
|
2308
2328
|
raw = "<system-reminder>纯噪声</system-reminder>"
|
|
2309
2329
|
req = self._build_request([{"role": "user", "content": raw}])
|
|
2310
|
-
assert _extract_session_title(req) == ""
|
|
2330
|
+
assert _extract_session_title(req) == "[Session] test"
|
|
2311
2331
|
|
|
2312
|
-
def
|
|
2332
|
+
def test_returns_metadata_fallback_for_no_user_messages(self):
|
|
2333
|
+
"""无 user 消息时回退到 Level 4 元数据兜底."""
|
|
2313
2334
|
req = self._build_request([{"role": "assistant", "content": "你好"}])
|
|
2314
|
-
assert _extract_session_title(req) == ""
|
|
2335
|
+
assert _extract_session_title(req) == "[Session] test"
|
|
2315
2336
|
|
|
2316
2337
|
def test_skips_noise_only_part_to_find_real_input(self):
|
|
2317
2338
|
"""首个 user text part 全噪声时,fallback 到下一个非空 user part."""
|
|
@@ -2338,3 +2359,266 @@ class TestExtractSessionTitle:
|
|
|
2338
2359
|
]
|
|
2339
2360
|
req = self._build_request(messages)
|
|
2340
2361
|
assert _extract_session_title(req) == "新的用户问题"
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
2365
|
+
# 多层级回退标题提取测试
|
|
2366
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
class TestExtractTitleFromUserText:
|
|
2370
|
+
"""Level 1 辅助函数 ``_extract_title_from_user_text``."""
|
|
2371
|
+
|
|
2372
|
+
def test_returns_first_non_empty_user_text(self):
|
|
2373
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2374
|
+
|
|
2375
|
+
msgs = [
|
|
2376
|
+
CanonicalMessagePart(
|
|
2377
|
+
type=CanonicalPartType.TEXT, role="user", text="用户输入"
|
|
2378
|
+
),
|
|
2379
|
+
]
|
|
2380
|
+
assert _extract_title_from_user_text(msgs) == "用户输入"
|
|
2381
|
+
|
|
2382
|
+
def test_skips_assistant_text(self):
|
|
2383
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2384
|
+
|
|
2385
|
+
msgs = [
|
|
2386
|
+
CanonicalMessagePart(
|
|
2387
|
+
type=CanonicalPartType.TEXT, role="assistant", text="助手回复"
|
|
2388
|
+
),
|
|
2389
|
+
CanonicalMessagePart(
|
|
2390
|
+
type=CanonicalPartType.TEXT, role="user", text="用户问题"
|
|
2391
|
+
),
|
|
2392
|
+
]
|
|
2393
|
+
assert _extract_title_from_user_text(msgs) == "用户问题"
|
|
2394
|
+
|
|
2395
|
+
def test_returns_empty_for_noise_only(self):
|
|
2396
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2397
|
+
|
|
2398
|
+
msgs = [
|
|
2399
|
+
CanonicalMessagePart(
|
|
2400
|
+
type=CanonicalPartType.TEXT,
|
|
2401
|
+
role="user",
|
|
2402
|
+
text="<system-reminder>纯噪声</system-reminder>",
|
|
2403
|
+
),
|
|
2404
|
+
]
|
|
2405
|
+
assert _extract_title_from_user_text(msgs) == ""
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
class TestExtractTitleFromToolResults:
|
|
2409
|
+
"""Level 2 辅助函数 ``_extract_title_from_tool_results``."""
|
|
2410
|
+
|
|
2411
|
+
def test_extracts_tool_result_text(self):
|
|
2412
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2413
|
+
|
|
2414
|
+
msgs = [
|
|
2415
|
+
CanonicalMessagePart(
|
|
2416
|
+
type=CanonicalPartType.TOOL_RESULT,
|
|
2417
|
+
role="user",
|
|
2418
|
+
text="file contents here",
|
|
2419
|
+
),
|
|
2420
|
+
]
|
|
2421
|
+
title = _extract_title_from_tool_results(msgs)
|
|
2422
|
+
assert title == "[Tool output] file contents here"
|
|
2423
|
+
|
|
2424
|
+
def test_skips_empty_tool_result(self):
|
|
2425
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2426
|
+
|
|
2427
|
+
msgs = [
|
|
2428
|
+
CanonicalMessagePart(
|
|
2429
|
+
type=CanonicalPartType.TOOL_RESULT, role="user", text=""
|
|
2430
|
+
),
|
|
2431
|
+
]
|
|
2432
|
+
assert _extract_title_from_tool_results(msgs) == ""
|
|
2433
|
+
|
|
2434
|
+
def test_truncates_long_tool_result(self):
|
|
2435
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2436
|
+
|
|
2437
|
+
long_text = "A" * 200
|
|
2438
|
+
msgs = [
|
|
2439
|
+
CanonicalMessagePart(
|
|
2440
|
+
type=CanonicalPartType.TOOL_RESULT, role="user", text=long_text
|
|
2441
|
+
),
|
|
2442
|
+
]
|
|
2443
|
+
title = _extract_title_from_tool_results(msgs)
|
|
2444
|
+
assert title.startswith("[Tool output] ")
|
|
2445
|
+
assert len(title) <= len("[Tool output] ") + _FALLBACK_TITLE_MAX_LEN
|
|
2446
|
+
|
|
2447
|
+
def test_sanitizes_noise_in_tool_result(self):
|
|
2448
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2449
|
+
|
|
2450
|
+
msgs = [
|
|
2451
|
+
CanonicalMessagePart(
|
|
2452
|
+
type=CanonicalPartType.TOOL_RESULT,
|
|
2453
|
+
role="user",
|
|
2454
|
+
text="<system-reminder>noise</system-reminder>clean output",
|
|
2455
|
+
),
|
|
2456
|
+
]
|
|
2457
|
+
title = _extract_title_from_tool_results(msgs)
|
|
2458
|
+
assert title == "[Tool output] clean output"
|
|
2459
|
+
|
|
2460
|
+
def test_returns_empty_when_all_noise(self):
|
|
2461
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2462
|
+
|
|
2463
|
+
msgs = [
|
|
2464
|
+
CanonicalMessagePart(
|
|
2465
|
+
type=CanonicalPartType.TOOL_RESULT,
|
|
2466
|
+
role="user",
|
|
2467
|
+
text="<system-reminder>纯噪声</system-reminder>",
|
|
2468
|
+
),
|
|
2469
|
+
]
|
|
2470
|
+
assert _extract_title_from_tool_results(msgs) == ""
|
|
2471
|
+
|
|
2472
|
+
|
|
2473
|
+
class TestExtractTitleFromImages:
|
|
2474
|
+
"""Level 3 辅助函数 ``_extract_title_from_images``."""
|
|
2475
|
+
|
|
2476
|
+
def test_single_image(self):
|
|
2477
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2478
|
+
|
|
2479
|
+
msgs = [
|
|
2480
|
+
CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
|
|
2481
|
+
]
|
|
2482
|
+
assert _extract_title_from_images(msgs) == "[1 Image]"
|
|
2483
|
+
|
|
2484
|
+
def test_multiple_images(self):
|
|
2485
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2486
|
+
|
|
2487
|
+
msgs = [
|
|
2488
|
+
CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
|
|
2489
|
+
CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
|
|
2490
|
+
CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="user"),
|
|
2491
|
+
]
|
|
2492
|
+
assert _extract_title_from_images(msgs) == "[3 Images]"
|
|
2493
|
+
|
|
2494
|
+
def test_no_images(self):
|
|
2495
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2496
|
+
|
|
2497
|
+
msgs = [
|
|
2498
|
+
CanonicalMessagePart(type=CanonicalPartType.TEXT, role="user", text="文本"),
|
|
2499
|
+
]
|
|
2500
|
+
assert _extract_title_from_images(msgs) == ""
|
|
2501
|
+
|
|
2502
|
+
def test_skips_assistant_images(self):
|
|
2503
|
+
from coding.proxy.model.compat import CanonicalMessagePart, CanonicalPartType
|
|
2504
|
+
|
|
2505
|
+
msgs = [
|
|
2506
|
+
CanonicalMessagePart(type=CanonicalPartType.IMAGE, role="assistant"),
|
|
2507
|
+
]
|
|
2508
|
+
assert _extract_title_from_images(msgs) == ""
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
class TestExtractTitleFromMetadata:
|
|
2512
|
+
"""Level 4 辅助函数 ``_extract_title_from_metadata``."""
|
|
2513
|
+
|
|
2514
|
+
@staticmethod
|
|
2515
|
+
def _build_request_with_meta(tool_names: list[str] | None = None, model: str = ""):
|
|
2516
|
+
body: dict = {"model": model, "messages": []}
|
|
2517
|
+
if tool_names:
|
|
2518
|
+
body["tools"] = [{"name": n} for n in tool_names]
|
|
2519
|
+
return build_canonical_request(body, {})
|
|
2520
|
+
|
|
2521
|
+
def test_uses_tool_names(self):
|
|
2522
|
+
req = self._build_request_with_meta(
|
|
2523
|
+
tool_names=["Bash", "Read", "Edit"], model="claude-opus-4-8"
|
|
2524
|
+
)
|
|
2525
|
+
assert _extract_title_from_metadata(req) == "[Tool call] Bash, Read, Edit"
|
|
2526
|
+
|
|
2527
|
+
def test_limits_to_three_tool_names(self):
|
|
2528
|
+
req = self._build_request_with_meta(
|
|
2529
|
+
tool_names=["Bash", "Read", "Edit", "Write", "Grep"], model="test"
|
|
2530
|
+
)
|
|
2531
|
+
assert _extract_title_from_metadata(req) == "[Tool call] Bash, Read, Edit"
|
|
2532
|
+
|
|
2533
|
+
def test_uses_model_when_no_tools(self):
|
|
2534
|
+
req = self._build_request_with_meta(tool_names=[], model="claude-sonnet-4-6")
|
|
2535
|
+
assert _extract_title_from_metadata(req) == "[Session] claude-sonnet-4-6"
|
|
2536
|
+
|
|
2537
|
+
def test_returns_empty_when_nothing(self):
|
|
2538
|
+
req = self._build_request_with_meta(tool_names=[], model="")
|
|
2539
|
+
assert _extract_title_from_metadata(req) == ""
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
class TestExtractSessionTitleFallback:
|
|
2543
|
+
"""``_extract_session_title`` 多层级回退集成测试."""
|
|
2544
|
+
|
|
2545
|
+
@staticmethod
|
|
2546
|
+
def _build_request(messages: list[dict], **extra):
|
|
2547
|
+
body: dict = {"model": "test-model", "messages": messages, **extra}
|
|
2548
|
+
return build_canonical_request(body, {})
|
|
2549
|
+
|
|
2550
|
+
def test_level1_takes_priority(self):
|
|
2551
|
+
"""Level 1 命中时不回退到 Level 2."""
|
|
2552
|
+
messages = [
|
|
2553
|
+
{
|
|
2554
|
+
"role": "user",
|
|
2555
|
+
"content": [
|
|
2556
|
+
{"type": "text", "text": "用户真实问题"},
|
|
2557
|
+
{
|
|
2558
|
+
"type": "tool_result",
|
|
2559
|
+
"tool_use_id": "tu_1",
|
|
2560
|
+
"content": "工具输出",
|
|
2561
|
+
},
|
|
2562
|
+
],
|
|
2563
|
+
}
|
|
2564
|
+
]
|
|
2565
|
+
req = self._build_request(messages)
|
|
2566
|
+
assert _extract_session_title(req) == "用户真实问题"
|
|
2567
|
+
|
|
2568
|
+
def test_level2_when_no_text(self):
|
|
2569
|
+
"""无 user TEXT 时,回退到 Level 2 TOOL_RESULT."""
|
|
2570
|
+
messages = [
|
|
2571
|
+
{
|
|
2572
|
+
"role": "user",
|
|
2573
|
+
"content": [
|
|
2574
|
+
{
|
|
2575
|
+
"type": "tool_result",
|
|
2576
|
+
"tool_use_id": "tu_1",
|
|
2577
|
+
"content": [{"type": "text", "text": "文件内容摘要"}],
|
|
2578
|
+
},
|
|
2579
|
+
],
|
|
2580
|
+
}
|
|
2581
|
+
]
|
|
2582
|
+
req = self._build_request(messages)
|
|
2583
|
+
assert _extract_session_title(req) == "[Tool output] 文件内容摘要"
|
|
2584
|
+
|
|
2585
|
+
def test_level3_when_only_images(self):
|
|
2586
|
+
"""无 TEXT 和 TOOL_RESULT 时,回退到 Level 3 IMAGE."""
|
|
2587
|
+
messages = [
|
|
2588
|
+
{
|
|
2589
|
+
"role": "user",
|
|
2590
|
+
"content": [
|
|
2591
|
+
{
|
|
2592
|
+
"type": "image",
|
|
2593
|
+
"source": {
|
|
2594
|
+
"type": "base64",
|
|
2595
|
+
"media_type": "image/png",
|
|
2596
|
+
"data": "abc",
|
|
2597
|
+
},
|
|
2598
|
+
},
|
|
2599
|
+
],
|
|
2600
|
+
}
|
|
2601
|
+
]
|
|
2602
|
+
req = self._build_request(messages)
|
|
2603
|
+
assert _extract_session_title(req) == "[1 Image]"
|
|
2604
|
+
|
|
2605
|
+
def test_level4_uses_tool_names(self):
|
|
2606
|
+
"""所有消息级别均无内容时,回退到 Level 4 元数据."""
|
|
2607
|
+
req = self._build_request([], tools=[{"name": "Bash"}, {"name": "Read"}])
|
|
2608
|
+
assert _extract_session_title(req) == "[Tool call] Bash, Read"
|
|
2609
|
+
|
|
2610
|
+
def test_level4_uses_model_name(self):
|
|
2611
|
+
"""无 tools 时,Level 4 使用 model 名称."""
|
|
2612
|
+
req = self._build_request([])
|
|
2613
|
+
assert _extract_session_title(req) == "[Session] test-model"
|
|
2614
|
+
|
|
2615
|
+
def test_fallback_cascade_full(self):
|
|
2616
|
+
"""Level 1 全噪声 → Level 2 全噪声 → Level 3 无图 → Level 4 模型名."""
|
|
2617
|
+
messages = [
|
|
2618
|
+
{
|
|
2619
|
+
"role": "user",
|
|
2620
|
+
"content": "<system-reminder>纯噪声</system-reminder>",
|
|
2621
|
+
},
|
|
2622
|
+
]
|
|
2623
|
+
req = self._build_request(messages)
|
|
2624
|
+
assert _extract_session_title(req) == "[Session] test-model"
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|