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