ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Claude API plugin configuration."""
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from ccproxy.models.provider import ModelCard, ModelMappingRule, ProviderConfig
|
|
6
|
+
from ccproxy.plugins.claude_shared.model_defaults import (
|
|
7
|
+
DEFAULT_CLAUDE_MODEL_CARDS,
|
|
8
|
+
DEFAULT_CLAUDE_MODEL_MAPPINGS,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClaudeAPISettings(ProviderConfig):
|
|
13
|
+
"""Claude API specific configuration.
|
|
14
|
+
|
|
15
|
+
This configuration extends the base ProviderConfig to include
|
|
16
|
+
Claude API specific settings like API endpoint and model support.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Base configuration from ProviderConfig
|
|
20
|
+
name: str = "claude-api"
|
|
21
|
+
base_url: str = "https://api.anthropic.com"
|
|
22
|
+
supports_streaming: bool = True
|
|
23
|
+
requires_auth: bool = True
|
|
24
|
+
auth_type: str = "oauth"
|
|
25
|
+
|
|
26
|
+
# Claude API specific settings
|
|
27
|
+
enabled: bool = True
|
|
28
|
+
priority: int = 5 # Higher priority than SDK-based approach
|
|
29
|
+
default_max_tokens: int = 4096
|
|
30
|
+
|
|
31
|
+
model_mappings: list[ModelMappingRule] = Field(
|
|
32
|
+
default_factory=lambda: [
|
|
33
|
+
rule.model_copy(deep=True) for rule in DEFAULT_CLAUDE_MODEL_MAPPINGS
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
models_endpoint: list[ModelCard] = Field(
|
|
37
|
+
default_factory=lambda: [
|
|
38
|
+
card.model_copy(deep=True) for card in DEFAULT_CLAUDE_MODEL_CARDS
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Feature flags
|
|
43
|
+
include_sdk_content_as_xml: bool = False
|
|
44
|
+
support_openai_format: bool = True # Support both Anthropic and OpenAI formats
|
|
45
|
+
|
|
46
|
+
# System prompt injection mode
|
|
47
|
+
system_prompt_injection_mode: str = "minimal" # "none", "minimal", or "full"
|
|
48
|
+
|
|
49
|
+
# NEW: Auth manager override support
|
|
50
|
+
auth_manager: str | None = (
|
|
51
|
+
None # Override auth manager name (e.g., 'oauth_claude_lb' for load balancing)
|
|
52
|
+
)
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""Claude API plugin detection service using centralized detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, Request, Response
|
|
14
|
+
|
|
15
|
+
from ccproxy.config.settings import Settings
|
|
16
|
+
from ccproxy.config.utils import get_ccproxy_cache_dir
|
|
17
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
18
|
+
from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
|
|
19
|
+
from ccproxy.services.cli_detection import CLIDetectionService
|
|
20
|
+
from ccproxy.utils.caching import async_ttl_cache
|
|
21
|
+
from ccproxy.utils.headers import extract_request_headers
|
|
22
|
+
|
|
23
|
+
from .models import ClaudeCacheData
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = get_plugin_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .models import ClaudeCliInfo
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClaudeAPIDetectionService:
|
|
34
|
+
"""Claude API plugin detection service for automatically detecting Claude CLI headers."""
|
|
35
|
+
|
|
36
|
+
# Headers to ignore at injection time (lowercase). Cache keeps keys (possibly empty) to preserve order.
|
|
37
|
+
ignores_header: list[str] = [
|
|
38
|
+
# Common excludes
|
|
39
|
+
"host",
|
|
40
|
+
"content-length",
|
|
41
|
+
"authorization",
|
|
42
|
+
"x-api-key",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
redact_headers: list[str] = [
|
|
46
|
+
"x-api-key",
|
|
47
|
+
"authorization",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
settings: Settings,
|
|
53
|
+
cli_service: CLIDetectionService | None = None,
|
|
54
|
+
redact_sensitive_cache: bool = True,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Initialize Claude detection service.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
settings: Application settings
|
|
60
|
+
cli_service: Optional CLIDetectionService instance for dependency injection.
|
|
61
|
+
If None, creates a new instance for backward compatibility.
|
|
62
|
+
"""
|
|
63
|
+
self.settings = settings
|
|
64
|
+
self.cache_dir = get_ccproxy_cache_dir()
|
|
65
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
self._cached_data: ClaudeCacheData | None = None
|
|
67
|
+
self._cli_service = cli_service or CLIDetectionService(settings)
|
|
68
|
+
self._cli_info: ClaudeCliInfo | None = None
|
|
69
|
+
self._redact_sensitive_cache = redact_sensitive_cache
|
|
70
|
+
|
|
71
|
+
async def initialize_detection(self) -> ClaudeCacheData:
|
|
72
|
+
"""Initialize Claude detection at startup."""
|
|
73
|
+
try:
|
|
74
|
+
# Get current Claude version
|
|
75
|
+
current_version = await self._get_claude_version()
|
|
76
|
+
|
|
77
|
+
# Try to load from cache first
|
|
78
|
+
cached = False
|
|
79
|
+
try:
|
|
80
|
+
detected_data = self._load_from_cache(current_version)
|
|
81
|
+
cached = detected_data is not None
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(
|
|
85
|
+
"invalid_cache_file",
|
|
86
|
+
error=str(e),
|
|
87
|
+
category="plugin",
|
|
88
|
+
exc_info=e,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not cached:
|
|
92
|
+
# No cache or version changed - detect fresh
|
|
93
|
+
detected_data = await self._detect_claude_headers(current_version)
|
|
94
|
+
# Cache the results
|
|
95
|
+
self._save_to_cache(detected_data)
|
|
96
|
+
|
|
97
|
+
self._cached_data = detected_data
|
|
98
|
+
|
|
99
|
+
logger.trace(
|
|
100
|
+
"detection_headers_completed",
|
|
101
|
+
version=current_version,
|
|
102
|
+
cached=cached,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if detected_data is None:
|
|
106
|
+
raise ValueError("Claude detection failed")
|
|
107
|
+
return detected_data
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning(
|
|
111
|
+
"detection_claude_headers_failed",
|
|
112
|
+
fallback=True,
|
|
113
|
+
error=e,
|
|
114
|
+
category="plugin",
|
|
115
|
+
)
|
|
116
|
+
# Return fallback data
|
|
117
|
+
fallback_data = self._get_fallback_data()
|
|
118
|
+
self._cached_data = fallback_data
|
|
119
|
+
return fallback_data
|
|
120
|
+
|
|
121
|
+
def get_cached_data(self) -> ClaudeCacheData | None:
|
|
122
|
+
"""Get currently cached detection data."""
|
|
123
|
+
return self._cached_data
|
|
124
|
+
|
|
125
|
+
def get_detected_headers(self) -> DetectedHeaders:
|
|
126
|
+
"""Return cached headers as structured data."""
|
|
127
|
+
|
|
128
|
+
data = self.get_cached_data()
|
|
129
|
+
if not data:
|
|
130
|
+
return DetectedHeaders()
|
|
131
|
+
return data.headers
|
|
132
|
+
|
|
133
|
+
def get_detected_prompts(self) -> DetectedPrompts:
|
|
134
|
+
"""Return cached prompt metadata as structured data."""
|
|
135
|
+
|
|
136
|
+
data = self.get_cached_data()
|
|
137
|
+
if not data:
|
|
138
|
+
return DetectedPrompts()
|
|
139
|
+
return data.prompts
|
|
140
|
+
|
|
141
|
+
def get_ignored_headers(self) -> list[str]:
|
|
142
|
+
"""Headers that should be ignored when injecting CLI values."""
|
|
143
|
+
|
|
144
|
+
return list(self.ignores_header)
|
|
145
|
+
|
|
146
|
+
def get_redacted_headers(self) -> list[str]:
|
|
147
|
+
"""Headers that must never be forwarded from detection cache."""
|
|
148
|
+
|
|
149
|
+
return list(self.redact_headers)
|
|
150
|
+
|
|
151
|
+
def get_cli_health_info(self) -> ClaudeCliInfo:
|
|
152
|
+
"""Get lightweight CLI health info using centralized detection, cached locally.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
ClaudeCliInfo with availability, version, and binary path
|
|
156
|
+
"""
|
|
157
|
+
from .models import ClaudeCliInfo, ClaudeCliStatus
|
|
158
|
+
|
|
159
|
+
if self._cli_info is not None:
|
|
160
|
+
return self._cli_info
|
|
161
|
+
|
|
162
|
+
info = self._cli_service.get_cli_info("claude")
|
|
163
|
+
status = (
|
|
164
|
+
ClaudeCliStatus.AVAILABLE
|
|
165
|
+
if info["is_available"]
|
|
166
|
+
else ClaudeCliStatus.NOT_INSTALLED
|
|
167
|
+
)
|
|
168
|
+
cli_info = ClaudeCliInfo(
|
|
169
|
+
status=status,
|
|
170
|
+
version=info.get("version"),
|
|
171
|
+
binary_path=info.get("path"),
|
|
172
|
+
)
|
|
173
|
+
self._cli_info = cli_info
|
|
174
|
+
return cli_info
|
|
175
|
+
|
|
176
|
+
def get_version(self) -> str | None:
|
|
177
|
+
"""Get the detected Claude CLI version."""
|
|
178
|
+
if self._cached_data:
|
|
179
|
+
return self._cached_data.claude_version
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def get_cli_path(self) -> list[str] | None:
|
|
183
|
+
"""Get the Claude CLI command with caching.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Command list to execute Claude CLI if found, None otherwise
|
|
187
|
+
"""
|
|
188
|
+
info = self._cli_service.get_cli_info("claude")
|
|
189
|
+
return info["command"] if info["is_available"] else None
|
|
190
|
+
|
|
191
|
+
def get_binary_path(self) -> list[str] | None:
|
|
192
|
+
"""Alias for get_cli_path for consistency with Codex."""
|
|
193
|
+
return self.get_cli_path()
|
|
194
|
+
|
|
195
|
+
@async_ttl_cache(maxsize=16, ttl=900.0) # 15 minute cache for version
|
|
196
|
+
async def _get_claude_version(self) -> str:
|
|
197
|
+
"""Get Claude CLI version with caching."""
|
|
198
|
+
try:
|
|
199
|
+
# Use centralized CLI detection
|
|
200
|
+
result = await self._cli_service.detect_cli(
|
|
201
|
+
binary_name="claude",
|
|
202
|
+
package_name="@anthropic-ai/claude-code",
|
|
203
|
+
version_flag="--version",
|
|
204
|
+
cache_key="claude_api_version",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if result.is_available and result.version:
|
|
208
|
+
return result.version
|
|
209
|
+
else:
|
|
210
|
+
raise FileNotFoundError("Claude CLI not found")
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(
|
|
214
|
+
"claude_version_detection_failed", error=str(e), category="plugin"
|
|
215
|
+
)
|
|
216
|
+
return "unknown"
|
|
217
|
+
|
|
218
|
+
async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
|
|
219
|
+
"""Execute Claude CLI with proxy to capture headers and system prompt."""
|
|
220
|
+
# Data captured from the request
|
|
221
|
+
captured_data: dict[str, Any] = {}
|
|
222
|
+
|
|
223
|
+
async def capture_handler(request: Request) -> Response:
|
|
224
|
+
"""Capture the Claude CLI request."""
|
|
225
|
+
# Capture request details
|
|
226
|
+
headers = extract_request_headers(request)
|
|
227
|
+
captured_data["headers"] = headers
|
|
228
|
+
captured_data["method"] = request.method
|
|
229
|
+
captured_data["url"] = str(request.url)
|
|
230
|
+
captured_data["path"] = request.url.path
|
|
231
|
+
captured_data["query_params"] = (
|
|
232
|
+
dict(request.query_params) if request.query_params else {}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
raw_body = await request.body()
|
|
236
|
+
captured_data["body"] = raw_body
|
|
237
|
+
# Try to parse to JSON for body_json
|
|
238
|
+
try:
|
|
239
|
+
captured_data["body_json"] = (
|
|
240
|
+
json.loads(raw_body.decode("utf-8")) if raw_body else None
|
|
241
|
+
)
|
|
242
|
+
except Exception:
|
|
243
|
+
captured_data["body_json"] = None
|
|
244
|
+
# Return a mock response to satisfy Claude CLI
|
|
245
|
+
return Response(
|
|
246
|
+
content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
|
|
247
|
+
media_type="application/json",
|
|
248
|
+
status_code=200,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Create temporary FastAPI app
|
|
252
|
+
temp_app = FastAPI()
|
|
253
|
+
temp_app.post("/v1/messages")(capture_handler)
|
|
254
|
+
|
|
255
|
+
# Find available port
|
|
256
|
+
sock = socket.socket()
|
|
257
|
+
sock.bind(("", 0))
|
|
258
|
+
port = sock.getsockname()[1]
|
|
259
|
+
sock.close()
|
|
260
|
+
|
|
261
|
+
# Start server in background
|
|
262
|
+
from uvicorn import Config, Server
|
|
263
|
+
|
|
264
|
+
config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
|
|
265
|
+
server = Server(config)
|
|
266
|
+
|
|
267
|
+
server_ready = asyncio.Event()
|
|
268
|
+
|
|
269
|
+
@temp_app.on_event("startup")
|
|
270
|
+
async def signal_server_ready() -> None:
|
|
271
|
+
"""Signal when the temporary detection server starts."""
|
|
272
|
+
|
|
273
|
+
server_ready.set()
|
|
274
|
+
|
|
275
|
+
server_task = asyncio.create_task(server.serve())
|
|
276
|
+
ready_task = asyncio.create_task(server_ready.wait())
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
done, _pending = await asyncio.wait(
|
|
280
|
+
{ready_task, server_task},
|
|
281
|
+
timeout=5,
|
|
282
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
283
|
+
)
|
|
284
|
+
if ready_task in done:
|
|
285
|
+
await ready_task
|
|
286
|
+
elif server_task in done:
|
|
287
|
+
await server_task
|
|
288
|
+
raise RuntimeError(
|
|
289
|
+
"Claude detection server exited before signalling readiness"
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
raise TimeoutError(
|
|
293
|
+
"Timed out waiting for Claude detection server startup"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
stdout, stderr = b"", b""
|
|
297
|
+
|
|
298
|
+
env: dict[str, str] = dict(os.environ)
|
|
299
|
+
env["ANTHROPIC_BASE_URL"] = f"http://127.0.0.1:{port}"
|
|
300
|
+
|
|
301
|
+
home_path = os.environ.get("HOME")
|
|
302
|
+
cwd_path = Path(home_path) if home_path else Path.cwd()
|
|
303
|
+
|
|
304
|
+
logger.debug(
|
|
305
|
+
"detection_service_using",
|
|
306
|
+
home_dir=home_path,
|
|
307
|
+
cwd=cwd_path,
|
|
308
|
+
category="plugin",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if home_path is not None:
|
|
312
|
+
env["HOME"] = home_path
|
|
313
|
+
|
|
314
|
+
cli_info = self._cli_service.get_cli_info("claude")
|
|
315
|
+
if not cli_info["is_available"] or not cli_info["command"]:
|
|
316
|
+
raise FileNotFoundError("Claude CLI not found for header detection")
|
|
317
|
+
|
|
318
|
+
cmd = cli_info["command"] + ["test"]
|
|
319
|
+
|
|
320
|
+
process = await asyncio.create_subprocess_exec(
|
|
321
|
+
*cmd,
|
|
322
|
+
env=env,
|
|
323
|
+
stdout=asyncio.subprocess.PIPE,
|
|
324
|
+
stderr=asyncio.subprocess.PIPE,
|
|
325
|
+
cwd=str(cwd_path),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
await asyncio.wait_for(process.wait(), timeout=30)
|
|
330
|
+
except TimeoutError:
|
|
331
|
+
process.kill()
|
|
332
|
+
await process.wait()
|
|
333
|
+
|
|
334
|
+
stdout = await process.stdout.read() if process.stdout else b""
|
|
335
|
+
stderr = await process.stderr.read() if process.stderr else b""
|
|
336
|
+
finally:
|
|
337
|
+
if not ready_task.done():
|
|
338
|
+
ready_task.cancel()
|
|
339
|
+
with suppress(asyncio.CancelledError):
|
|
340
|
+
await ready_task
|
|
341
|
+
|
|
342
|
+
server.should_exit = True
|
|
343
|
+
await server_task
|
|
344
|
+
|
|
345
|
+
if not captured_data:
|
|
346
|
+
logger.error(
|
|
347
|
+
"failed_to_capture_claude_cli_request",
|
|
348
|
+
stdout=stdout.decode(errors="ignore"),
|
|
349
|
+
stderr=stderr.decode(errors="ignore"),
|
|
350
|
+
category="plugin",
|
|
351
|
+
)
|
|
352
|
+
raise RuntimeError("Failed to capture Claude CLI request")
|
|
353
|
+
|
|
354
|
+
headers_dict = (
|
|
355
|
+
self._sanitize_headers_for_cache(captured_data["headers"])
|
|
356
|
+
if self._redact_sensitive_cache
|
|
357
|
+
else captured_data["headers"]
|
|
358
|
+
)
|
|
359
|
+
body_json = (
|
|
360
|
+
self._sanitize_body_json_for_cache(captured_data.get("body_json"))
|
|
361
|
+
if self._redact_sensitive_cache
|
|
362
|
+
else captured_data.get("body_json")
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
prompts = DetectedPrompts.from_body(body_json)
|
|
366
|
+
|
|
367
|
+
return ClaudeCacheData(
|
|
368
|
+
claude_version=version,
|
|
369
|
+
headers=DetectedHeaders(headers_dict),
|
|
370
|
+
prompts=prompts,
|
|
371
|
+
body_json=body_json,
|
|
372
|
+
method=captured_data.get("method"),
|
|
373
|
+
url=captured_data.get("url"),
|
|
374
|
+
path=captured_data.get("path"),
|
|
375
|
+
query_params=captured_data.get("query_params"),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
|
|
379
|
+
"""Load cached data for specific Claude version."""
|
|
380
|
+
cache_file = self.cache_dir / f"claude_headers_{version}.json"
|
|
381
|
+
|
|
382
|
+
if not cache_file.exists():
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
with cache_file.open("r") as f:
|
|
386
|
+
data = json.load(f)
|
|
387
|
+
return ClaudeCacheData.model_validate(data)
|
|
388
|
+
|
|
389
|
+
def _save_to_cache(self, data: ClaudeCacheData) -> None:
|
|
390
|
+
"""Save detection data to cache."""
|
|
391
|
+
cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
with cache_file.open("w") as f:
|
|
395
|
+
json.dump(data.model_dump(), f, indent=2, default=str)
|
|
396
|
+
logger.debug(
|
|
397
|
+
"cache_saved",
|
|
398
|
+
file=str(cache_file),
|
|
399
|
+
version=data.claude_version,
|
|
400
|
+
category="plugin",
|
|
401
|
+
)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.warning(
|
|
404
|
+
"cache_save_failed",
|
|
405
|
+
file=str(cache_file),
|
|
406
|
+
error=str(e),
|
|
407
|
+
category="plugin",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _get_fallback_data(self) -> ClaudeCacheData:
|
|
411
|
+
"""Get fallback data when detection fails."""
|
|
412
|
+
logger.warning("using_fallback_claude_data", category="plugin")
|
|
413
|
+
|
|
414
|
+
# Load fallback data from package data file
|
|
415
|
+
package_data_file = (
|
|
416
|
+
Path(__file__).resolve().parents[2]
|
|
417
|
+
/ "data"
|
|
418
|
+
/ "claude_headers_fallback.json"
|
|
419
|
+
)
|
|
420
|
+
with package_data_file.open("r") as f:
|
|
421
|
+
fallback_data_dict = json.load(f)
|
|
422
|
+
return ClaudeCacheData.model_validate(fallback_data_dict)
|
|
423
|
+
|
|
424
|
+
def invalidate_cache(self) -> None:
|
|
425
|
+
"""Clear all cached detection data."""
|
|
426
|
+
# Clear the async cache for _get_claude_version
|
|
427
|
+
if hasattr(self._get_claude_version, "cache_clear"):
|
|
428
|
+
self._get_claude_version.cache_clear()
|
|
429
|
+
# Clear CLI info cache
|
|
430
|
+
self._cli_info = None
|
|
431
|
+
logger.debug("detection_cache_cleared", category="plugin")
|
|
432
|
+
|
|
433
|
+
# --- Helpers ---
|
|
434
|
+
def _sanitize_headers_for_cache(self, headers: dict[str, str]) -> dict[str, str]:
|
|
435
|
+
"""Redact sensitive headers for cache while preserving keys and order."""
|
|
436
|
+
# Build ordered dict copy
|
|
437
|
+
sanitized: dict[str, str] = {}
|
|
438
|
+
for k, v in headers.items():
|
|
439
|
+
lk = k.lower()
|
|
440
|
+
if lk in {"authorization", "host"}:
|
|
441
|
+
sanitized[lk] = ""
|
|
442
|
+
else:
|
|
443
|
+
sanitized[lk] = v
|
|
444
|
+
return sanitized
|
|
445
|
+
|
|
446
|
+
def _sanitize_body_json_for_cache(
|
|
447
|
+
self, body: dict[str, Any] | None
|
|
448
|
+
) -> dict[str, Any] | None:
|
|
449
|
+
if body is None:
|
|
450
|
+
return None
|
|
451
|
+
# For Claude, no specific fields to redact currently; return as-is
|
|
452
|
+
return body
|
|
453
|
+
|
|
454
|
+
def get_system_prompt(self, mode: str | None = "minimal") -> dict[str, Any]:
|
|
455
|
+
"""Return a system prompt dict for injection based on cached prompts.
|
|
456
|
+
|
|
457
|
+
mode: "none", "minimal", or "full"
|
|
458
|
+
"""
|
|
459
|
+
prompts = self.get_detected_prompts()
|
|
460
|
+
mode_value = "full" if mode is None else mode
|
|
461
|
+
return prompts.system_payload(mode=mode_value)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Claude API plugin health check implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.core.plugins.protocol import HealthCheckResult
|
|
7
|
+
from ccproxy.plugins.oauth_claude.manager import ClaudeApiTokenManager
|
|
8
|
+
|
|
9
|
+
from .config import ClaudeAPISettings
|
|
10
|
+
from .detection_service import ClaudeAPIDetectionService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_plugin_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def claude_api_health_check(
|
|
17
|
+
config: ClaudeAPISettings | None,
|
|
18
|
+
detection_service: ClaudeAPIDetectionService | None = None,
|
|
19
|
+
credentials_manager: ClaudeApiTokenManager | None = None,
|
|
20
|
+
*,
|
|
21
|
+
version: str,
|
|
22
|
+
) -> HealthCheckResult:
|
|
23
|
+
"""Perform health check for Claude API plugin.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Plugin configuration
|
|
27
|
+
credentials_manager: Token manager for OAuth token status
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
HealthCheckResult with plugin status including OAuth token details
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
if not config:
|
|
34
|
+
return HealthCheckResult(
|
|
35
|
+
status="fail",
|
|
36
|
+
componentId="plugin-claude-api",
|
|
37
|
+
componentType="provider_plugin",
|
|
38
|
+
output="Claude API plugin configuration not available",
|
|
39
|
+
version=version,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Check if plugin is enabled
|
|
43
|
+
if not config.enabled:
|
|
44
|
+
return HealthCheckResult(
|
|
45
|
+
status="warn",
|
|
46
|
+
componentId="plugin-claude-api",
|
|
47
|
+
componentType="provider_plugin",
|
|
48
|
+
output="Claude API plugin is disabled",
|
|
49
|
+
version=version,
|
|
50
|
+
details={"enabled": False},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Check basic configuration
|
|
54
|
+
if not config.base_url:
|
|
55
|
+
return HealthCheckResult(
|
|
56
|
+
status="fail",
|
|
57
|
+
componentId="plugin-claude-api",
|
|
58
|
+
componentType="provider_plugin",
|
|
59
|
+
output="Claude API base URL not configured",
|
|
60
|
+
version=version,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Standardized details
|
|
64
|
+
from ccproxy.core.plugins.models import (
|
|
65
|
+
AuthHealth,
|
|
66
|
+
CLIHealth,
|
|
67
|
+
ConfigHealth,
|
|
68
|
+
ProviderHealthDetails,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
cli_info = (
|
|
72
|
+
detection_service.get_cli_health_info() if detection_service else None
|
|
73
|
+
)
|
|
74
|
+
cli_health = (
|
|
75
|
+
CLIHealth(
|
|
76
|
+
available=bool(
|
|
77
|
+
cli_info
|
|
78
|
+
and getattr(cli_info, "status", None)
|
|
79
|
+
== getattr(cli_info.__class__, "__members__", {}).get("AVAILABLE")
|
|
80
|
+
),
|
|
81
|
+
status=(cli_info.status.value if cli_info else "unknown"),
|
|
82
|
+
version=(cli_info.version if cli_info else None),
|
|
83
|
+
path=(cli_info.binary_path if cli_info else None),
|
|
84
|
+
)
|
|
85
|
+
if cli_info
|
|
86
|
+
else None
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
auth_raw: dict[str, Any] = {}
|
|
90
|
+
if credentials_manager:
|
|
91
|
+
try:
|
|
92
|
+
auth_raw = await credentials_manager.get_auth_status()
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.debug("auth_status_failed", error=str(e), category="auth")
|
|
95
|
+
auth_raw = {"authenticated": False, "reason": str(e)}
|
|
96
|
+
|
|
97
|
+
auth_health = (
|
|
98
|
+
AuthHealth(
|
|
99
|
+
configured=bool(credentials_manager),
|
|
100
|
+
token_available=auth_raw.get("authenticated"),
|
|
101
|
+
token_expired=(
|
|
102
|
+
not auth_raw.get("authenticated")
|
|
103
|
+
and auth_raw.get("reason") == "Token expired"
|
|
104
|
+
),
|
|
105
|
+
account_id=auth_raw.get("account_id"),
|
|
106
|
+
expires_at=auth_raw.get("expires_at"),
|
|
107
|
+
error=(
|
|
108
|
+
None if auth_raw.get("authenticated") else auth_raw.get("reason")
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
if credentials_manager
|
|
112
|
+
else AuthHealth(configured=False)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
config_health = ConfigHealth(
|
|
116
|
+
model_count=len(config.models_endpoint) if config.models_endpoint else 0,
|
|
117
|
+
supports_openai_format=config.support_openai_format,
|
|
118
|
+
extra=None,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Compose output message
|
|
122
|
+
status: Literal["pass", "warn", "fail"]
|
|
123
|
+
output_parts: list[str] = []
|
|
124
|
+
if auth_health.token_available and not auth_health.token_expired:
|
|
125
|
+
output_parts.append("Authenticated")
|
|
126
|
+
status = "pass"
|
|
127
|
+
elif auth_health.token_expired:
|
|
128
|
+
output_parts.append("Token expired")
|
|
129
|
+
status = "warn"
|
|
130
|
+
elif auth_health.configured:
|
|
131
|
+
output_parts.append("Auth configured but token unavailable")
|
|
132
|
+
status = "warn"
|
|
133
|
+
else:
|
|
134
|
+
output_parts.append("Authentication not configured")
|
|
135
|
+
status = "warn"
|
|
136
|
+
|
|
137
|
+
if cli_health and cli_health.available:
|
|
138
|
+
output_parts.append(
|
|
139
|
+
f"CLI v{cli_health.version}" if cli_health.version else "CLI available"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
output_parts.append("CLI not found")
|
|
143
|
+
|
|
144
|
+
if config.models_endpoint:
|
|
145
|
+
output_parts.append(f"{len(config.models_endpoint)} models available")
|
|
146
|
+
|
|
147
|
+
output = "Claude API: " + ", ".join(output_parts)
|
|
148
|
+
|
|
149
|
+
details_model = ProviderHealthDetails(
|
|
150
|
+
provider="claude_api",
|
|
151
|
+
enabled=config.enabled,
|
|
152
|
+
base_url=config.base_url,
|
|
153
|
+
cli=cli_health,
|
|
154
|
+
auth=auth_health,
|
|
155
|
+
config=config_health,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return HealthCheckResult(
|
|
159
|
+
status=status,
|
|
160
|
+
componentId="plugin-claude-api",
|
|
161
|
+
componentType="provider_plugin",
|
|
162
|
+
output=output,
|
|
163
|
+
version=version,
|
|
164
|
+
details=details_model.model_dump(),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error("health_check_failed", error=str(e))
|
|
169
|
+
return HealthCheckResult(
|
|
170
|
+
status="fail",
|
|
171
|
+
componentId="plugin-claude-api",
|
|
172
|
+
componentType="provider_plugin",
|
|
173
|
+
output=f"Claude API health check failed: {str(e)}",
|
|
174
|
+
version=version,
|
|
175
|
+
)
|