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
ccproxy/api/app.py
CHANGED
|
@@ -1,59 +1,85 @@
|
|
|
1
|
-
"""FastAPI application factory for CCProxy API Server."""
|
|
1
|
+
"""FastAPI application factory for CCProxy API Server with plugin system."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
|
-
from
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
from fastapi
|
|
9
|
-
from
|
|
8
|
+
import structlog
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.routing import APIRouter
|
|
11
|
+
from typing_extensions import TypedDict
|
|
10
12
|
|
|
11
|
-
from ccproxy import
|
|
13
|
+
from ccproxy.api.bootstrap import create_service_container
|
|
14
|
+
from ccproxy.api.format_validation import validate_route_format_chains
|
|
12
15
|
from ccproxy.api.middleware.cors import setup_cors_middleware
|
|
13
16
|
from ccproxy.api.middleware.errors import setup_error_handlers
|
|
14
|
-
from ccproxy.api.middleware.logging import AccessLogMiddleware
|
|
15
|
-
from ccproxy.api.middleware.request_content_logging import (
|
|
16
|
-
RequestContentLoggingMiddleware,
|
|
17
|
-
)
|
|
18
|
-
from ccproxy.api.middleware.request_id import RequestIDMiddleware
|
|
19
|
-
from ccproxy.api.middleware.server_header import ServerHeaderMiddleware
|
|
20
|
-
from ccproxy.api.routes.claude import router as claude_router
|
|
21
|
-
from ccproxy.api.routes.codex import router as codex_router
|
|
22
17
|
from ccproxy.api.routes.health import router as health_router
|
|
23
|
-
from ccproxy.api.routes.
|
|
24
|
-
from ccproxy.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
from ccproxy.api.routes.plugins import router as plugins_router
|
|
19
|
+
from ccproxy.auth.oauth.router import oauth_router
|
|
20
|
+
from ccproxy.config.settings import Settings
|
|
21
|
+
from ccproxy.core import __version__
|
|
22
|
+
from ccproxy.core.async_task_manager import start_task_manager, stop_task_manager
|
|
23
|
+
from ccproxy.core.logging import TraceBoundLogger, get_logger, setup_logging
|
|
24
|
+
from ccproxy.core.plugins import (
|
|
25
|
+
MiddlewareManager,
|
|
26
|
+
PluginRegistry,
|
|
27
|
+
load_plugin_system,
|
|
28
|
+
setup_default_middleware,
|
|
28
29
|
)
|
|
29
|
-
from ccproxy.
|
|
30
|
-
from ccproxy.
|
|
31
|
-
from ccproxy.
|
|
32
|
-
from ccproxy.config.settings import Settings, get_settings
|
|
33
|
-
from ccproxy.core.logging import setup_logging
|
|
34
|
-
from ccproxy.utils.models_provider import get_models_list
|
|
30
|
+
from ccproxy.core.plugins.hooks import HookManager
|
|
31
|
+
from ccproxy.core.plugins.hooks.events import HookEvent
|
|
32
|
+
from ccproxy.services.container import ServiceContainer
|
|
35
33
|
from ccproxy.utils.startup_helpers import (
|
|
36
34
|
check_claude_cli_startup,
|
|
37
|
-
|
|
38
|
-
flush_streaming_batches_shutdown,
|
|
39
|
-
initialize_claude_detection_startup,
|
|
40
|
-
initialize_claude_sdk_startup,
|
|
41
|
-
initialize_codex_detection_startup,
|
|
42
|
-
initialize_log_storage_shutdown,
|
|
43
|
-
initialize_log_storage_startup,
|
|
44
|
-
initialize_permission_service_startup,
|
|
45
|
-
setup_permission_service_shutdown,
|
|
35
|
+
check_version_updates_startup,
|
|
46
36
|
setup_scheduler_shutdown,
|
|
47
37
|
setup_scheduler_startup,
|
|
48
|
-
setup_session_manager_shutdown,
|
|
49
|
-
validate_authentication_startup,
|
|
50
38
|
)
|
|
51
39
|
|
|
52
40
|
|
|
53
|
-
logger = get_logger(
|
|
41
|
+
logger: TraceBoundLogger = get_logger()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def merge_router_tags(
|
|
45
|
+
router: APIRouter,
|
|
46
|
+
spec_tags: list[str] | None = None,
|
|
47
|
+
default_tags: list[str] | None = None,
|
|
48
|
+
) -> list[str | Enum] | None:
|
|
49
|
+
"""Merge router tags with spec tags, removing duplicates while preserving order.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
router: FastAPI router instance
|
|
53
|
+
spec_tags: Tags from route specification
|
|
54
|
+
default_tags: Fallback tags if no other tags exist
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Deduplicated list of tags, or None if no tags
|
|
58
|
+
"""
|
|
59
|
+
router_tags: list[str | Enum] = list(router.tags) if router.tags else []
|
|
60
|
+
spec_tags_list: list[str | Enum] = list(spec_tags) if spec_tags else []
|
|
61
|
+
default_tags_list: list[str | Enum] = list(default_tags) if default_tags else []
|
|
62
|
+
|
|
63
|
+
# Only use defaults if no other tags exist
|
|
64
|
+
if not router_tags and not spec_tags_list and default_tags_list:
|
|
65
|
+
return default_tags_list
|
|
66
|
+
|
|
67
|
+
# Merge all non-default tags and deduplicate
|
|
68
|
+
all_tags: list[str | Enum] = router_tags + spec_tags_list
|
|
69
|
+
if not all_tags:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
# Deduplicate by string value while preserving order
|
|
73
|
+
unique: list[str | Enum] = []
|
|
74
|
+
seen: set[str] = set()
|
|
75
|
+
for t in all_tags:
|
|
76
|
+
s = str(t)
|
|
77
|
+
if s not in seen:
|
|
78
|
+
seen.add(s)
|
|
79
|
+
unique.append(t)
|
|
80
|
+
return unique
|
|
54
81
|
|
|
55
82
|
|
|
56
|
-
# Type definitions for lifecycle components
|
|
57
83
|
class LifecycleComponent(TypedDict):
|
|
58
84
|
name: str
|
|
59
85
|
startup: Callable[[FastAPI, Any], Awaitable[None]] | None
|
|
@@ -69,37 +95,159 @@ class ShutdownComponent(TypedDict):
|
|
|
69
95
|
shutdown: Callable[[FastAPI], Awaitable[None]] | None
|
|
70
96
|
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
async def setup_task_manager_startup(app: FastAPI, settings: Settings) -> None:
|
|
99
|
+
"""Start the async task manager."""
|
|
100
|
+
container: ServiceContainer = app.state.service_container
|
|
101
|
+
await start_task_manager(container=container)
|
|
102
|
+
logger.debug("task_manager_startup_completed", category="lifecycle")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def setup_task_manager_shutdown(app: FastAPI) -> None:
|
|
106
|
+
"""Stop the async task manager."""
|
|
107
|
+
container: ServiceContainer = app.state.service_container
|
|
108
|
+
await stop_task_manager(container=container)
|
|
109
|
+
logger.debug("task_manager_shutdown_completed", category="lifecycle")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def setup_service_container_shutdown(app: FastAPI) -> None:
|
|
113
|
+
"""Close the service container and its resources."""
|
|
114
|
+
if hasattr(app.state, "service_container"):
|
|
115
|
+
service_container = app.state.service_container
|
|
116
|
+
await service_container.shutdown()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def initialize_plugins_startup(app: FastAPI, settings: Settings) -> None:
|
|
120
|
+
"""Initialize plugins during startup (runtime phase)."""
|
|
121
|
+
if not settings.enable_plugins:
|
|
122
|
+
logger.info("plugin_system_disabled", category="lifecycle")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if not hasattr(app.state, "plugin_registry"):
|
|
126
|
+
logger.warning("plugin_registry_not_found", category="lifecycle")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
plugin_registry: PluginRegistry = app.state.plugin_registry
|
|
130
|
+
service_container: ServiceContainer = app.state.service_container
|
|
131
|
+
|
|
132
|
+
hook_registry = service_container.get_hook_registry()
|
|
133
|
+
background_thread_manager = service_container.get_background_hook_thread_manager()
|
|
134
|
+
hook_manager = HookManager(hook_registry, background_thread_manager)
|
|
135
|
+
app.state.hook_registry = hook_registry
|
|
136
|
+
app.state.hook_manager = hook_manager
|
|
137
|
+
service_container.register_service(HookManager, instance=hook_manager)
|
|
138
|
+
|
|
139
|
+
# StreamingHandler now requires HookManager at construction via DI factory,
|
|
140
|
+
# so no post-hoc patching is needed here.
|
|
141
|
+
|
|
142
|
+
# Perform manifest population with access to http_pool_manager
|
|
143
|
+
# This allows plugins to modify their manifests during context creation
|
|
144
|
+
for plugin_name, factory in plugin_registry.factories.items():
|
|
145
|
+
try:
|
|
146
|
+
factory.create_context(service_container)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.warning(
|
|
149
|
+
"plugin_context_creation_failed",
|
|
150
|
+
plugin=plugin_name,
|
|
151
|
+
error=str(e),
|
|
152
|
+
exc_info=e,
|
|
153
|
+
category="plugin",
|
|
154
|
+
)
|
|
155
|
+
# Continue with other plugins
|
|
156
|
+
|
|
157
|
+
await plugin_registry.initialize_all(service_container)
|
|
158
|
+
# A consolidated summary is already emitted by PluginRegistry.initialize_all()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def shutdown_plugins(app: FastAPI) -> None:
|
|
162
|
+
"""Shutdown plugins."""
|
|
163
|
+
if hasattr(app.state, "plugin_registry"):
|
|
164
|
+
plugin_registry: PluginRegistry = app.state.plugin_registry
|
|
165
|
+
await plugin_registry.shutdown_all()
|
|
166
|
+
logger.debug("plugins_shutdown_completed", category="lifecycle")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def shutdown_hook_system(app: FastAPI) -> None:
|
|
170
|
+
"""Shutdown the hook system and background thread."""
|
|
171
|
+
try:
|
|
172
|
+
# Get hook manager from app state - it will shutdown its own background manager
|
|
173
|
+
hook_manager = getattr(app.state, "hook_manager", None)
|
|
174
|
+
if hook_manager:
|
|
175
|
+
hook_manager.shutdown()
|
|
176
|
+
|
|
177
|
+
logger.debug("hook_system_shutdown_completed", category="lifecycle")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(
|
|
180
|
+
"hook_system_shutdown_failed",
|
|
181
|
+
error=str(e),
|
|
182
|
+
category="lifecycle",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def initialize_hooks_startup(app: FastAPI, settings: Settings) -> None:
|
|
187
|
+
"""Initialize hook system with plugins."""
|
|
188
|
+
if hasattr(app.state, "hook_registry") and hasattr(app.state, "hook_manager"):
|
|
189
|
+
hook_registry = app.state.hook_registry
|
|
190
|
+
hook_manager = app.state.hook_manager
|
|
191
|
+
logger.debug("hook_system_already_created", category="lifecycle")
|
|
192
|
+
else:
|
|
193
|
+
service_container: ServiceContainer = app.state.service_container
|
|
194
|
+
hook_registry = service_container.get_hook_registry()
|
|
195
|
+
background_thread_manager = (
|
|
196
|
+
service_container.get_background_hook_thread_manager()
|
|
197
|
+
)
|
|
198
|
+
hook_manager = HookManager(hook_registry, background_thread_manager)
|
|
199
|
+
app.state.hook_registry = hook_registry
|
|
200
|
+
app.state.hook_manager = hook_manager
|
|
201
|
+
|
|
202
|
+
# Register plugin hooks
|
|
203
|
+
if hasattr(app.state, "plugin_registry"):
|
|
204
|
+
plugin_registry: PluginRegistry = app.state.plugin_registry
|
|
205
|
+
|
|
206
|
+
for name, factory in plugin_registry.factories.items():
|
|
207
|
+
manifest = factory.get_manifest()
|
|
208
|
+
for hook_spec in manifest.hooks:
|
|
209
|
+
try:
|
|
210
|
+
hook_instance = hook_spec.hook_class(**hook_spec.kwargs)
|
|
211
|
+
hook_registry.register(hook_instance)
|
|
212
|
+
logger.debug(
|
|
213
|
+
"plugin_hook_registered",
|
|
214
|
+
plugin_name=name,
|
|
215
|
+
hook_class=hook_spec.hook_class.__name__,
|
|
216
|
+
category="lifecycle",
|
|
217
|
+
)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(
|
|
220
|
+
"plugin_hook_registration_failed",
|
|
221
|
+
plugin_name=name,
|
|
222
|
+
hook_class=hook_spec.hook_class.__name__,
|
|
223
|
+
error=str(e),
|
|
224
|
+
exc_info=e,
|
|
225
|
+
category="lifecycle",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
await hook_manager.emit(HookEvent.APP_STARTUP, {"phase": "startup"})
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(
|
|
232
|
+
"startup_hook_failed", error=str(e), exc_info=e, category="lifecycle"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
73
236
|
LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
|
|
74
237
|
{
|
|
75
|
-
"name": "
|
|
76
|
-
"startup":
|
|
77
|
-
"shutdown":
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"name": "Claude CLI",
|
|
81
|
-
"startup": check_claude_cli_startup,
|
|
82
|
-
"shutdown": None, # Detection only, no cleanup needed
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
"name": "Codex CLI",
|
|
86
|
-
"startup": check_codex_cli_startup,
|
|
87
|
-
"shutdown": None, # Detection only, no cleanup needed
|
|
238
|
+
"name": "Task Manager",
|
|
239
|
+
"startup": setup_task_manager_startup,
|
|
240
|
+
"shutdown": setup_task_manager_shutdown,
|
|
88
241
|
},
|
|
89
242
|
{
|
|
90
|
-
"name": "
|
|
91
|
-
"startup":
|
|
92
|
-
"shutdown": None,
|
|
243
|
+
"name": "Version Check",
|
|
244
|
+
"startup": check_version_updates_startup,
|
|
245
|
+
"shutdown": None,
|
|
93
246
|
},
|
|
94
247
|
{
|
|
95
|
-
"name": "
|
|
96
|
-
"startup":
|
|
97
|
-
"shutdown": None,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"name": "Claude SDK",
|
|
101
|
-
"startup": initialize_claude_sdk_startup,
|
|
102
|
-
"shutdown": setup_session_manager_shutdown,
|
|
248
|
+
"name": "Claude CLI",
|
|
249
|
+
"startup": check_claude_cli_startup,
|
|
250
|
+
"shutdown": None,
|
|
103
251
|
},
|
|
104
252
|
{
|
|
105
253
|
"name": "Scheduler",
|
|
@@ -107,154 +255,190 @@ LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
|
|
|
107
255
|
"shutdown": setup_scheduler_shutdown,
|
|
108
256
|
},
|
|
109
257
|
{
|
|
110
|
-
"name": "
|
|
111
|
-
"startup":
|
|
112
|
-
"shutdown":
|
|
258
|
+
"name": "Service Container",
|
|
259
|
+
"startup": None,
|
|
260
|
+
"shutdown": setup_service_container_shutdown,
|
|
113
261
|
},
|
|
114
262
|
{
|
|
115
|
-
"name": "
|
|
116
|
-
"startup":
|
|
117
|
-
"shutdown":
|
|
263
|
+
"name": "Plugin System",
|
|
264
|
+
"startup": initialize_plugins_startup,
|
|
265
|
+
"shutdown": shutdown_plugins,
|
|
118
266
|
},
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
# Additional shutdown-only components that need special handling
|
|
122
|
-
SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = [
|
|
123
267
|
{
|
|
124
|
-
"name": "
|
|
125
|
-
"
|
|
268
|
+
"name": "Hook System",
|
|
269
|
+
"startup": initialize_hooks_startup,
|
|
270
|
+
"shutdown": shutdown_hook_system,
|
|
126
271
|
},
|
|
127
272
|
]
|
|
128
273
|
|
|
129
|
-
|
|
130
|
-
# Create shared models router
|
|
131
|
-
models_router = APIRouter(tags=["models"])
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@models_router.get("/v1/models", response_model=None)
|
|
135
|
-
async def list_models() -> dict[str, Any]:
|
|
136
|
-
"""List available models.
|
|
137
|
-
|
|
138
|
-
Returns a combined list of Anthropic models and recent OpenAI models.
|
|
139
|
-
This endpoint is shared between both SDK and proxy APIs.
|
|
140
|
-
"""
|
|
141
|
-
return get_models_list()
|
|
274
|
+
SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = []
|
|
142
275
|
|
|
143
276
|
|
|
144
277
|
@asynccontextmanager
|
|
145
278
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
146
279
|
"""Application lifespan manager using component-based approach."""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# Store settings in app state for reuse in dependencies
|
|
150
|
-
app.state.settings = settings
|
|
151
|
-
|
|
152
|
-
# Startup
|
|
280
|
+
service_container: ServiceContainer = app.state.service_container
|
|
281
|
+
settings = service_container.get_service(Settings)
|
|
153
282
|
logger.info(
|
|
154
|
-
"
|
|
283
|
+
"server_starting",
|
|
155
284
|
host=settings.server.host,
|
|
156
285
|
port=settings.server.port,
|
|
157
286
|
url=f"http://{settings.server.host}:{settings.server.port}",
|
|
287
|
+
category="lifecycle",
|
|
158
288
|
)
|
|
289
|
+
# Demote granular config detail to DEBUG
|
|
159
290
|
logger.debug(
|
|
160
|
-
"server_configured",
|
|
291
|
+
"server_configured",
|
|
292
|
+
host=settings.server.host,
|
|
293
|
+
port=settings.server.port,
|
|
294
|
+
category="config",
|
|
161
295
|
)
|
|
162
296
|
|
|
163
|
-
# Log Claude CLI configuration
|
|
164
|
-
if settings.claude.cli_path:
|
|
165
|
-
logger.debug("claude_cli_configured", cli_path=settings.claude.cli_path)
|
|
166
|
-
else:
|
|
167
|
-
logger.debug("claude_cli_auto_detect")
|
|
168
|
-
logger.debug(
|
|
169
|
-
"claude_cli_search_paths", paths=settings.claude.get_searched_paths()
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# Execute startup components in order
|
|
173
297
|
for component in LIFECYCLE_COMPONENTS:
|
|
174
298
|
if component["startup"]:
|
|
175
299
|
component_name = component["name"]
|
|
176
300
|
try:
|
|
177
|
-
logger.debug(
|
|
301
|
+
logger.debug(
|
|
302
|
+
f"starting_{component_name.lower().replace(' ', '_')}",
|
|
303
|
+
category="lifecycle",
|
|
304
|
+
)
|
|
178
305
|
await component["startup"](app, settings)
|
|
306
|
+
except (OSError, PermissionError) as e:
|
|
307
|
+
logger.error(
|
|
308
|
+
f"{component_name.lower().replace(' ', '_')}_startup_io_failed",
|
|
309
|
+
error=str(e),
|
|
310
|
+
component=component_name,
|
|
311
|
+
exc_info=e,
|
|
312
|
+
category="lifecycle",
|
|
313
|
+
)
|
|
179
314
|
except Exception as e:
|
|
180
315
|
logger.error(
|
|
181
316
|
f"{component_name.lower().replace(' ', '_')}_startup_failed",
|
|
182
317
|
error=str(e),
|
|
183
318
|
component=component_name,
|
|
319
|
+
exc_info=e,
|
|
320
|
+
category="lifecycle",
|
|
184
321
|
)
|
|
185
|
-
|
|
322
|
+
|
|
323
|
+
# After startup completes (post-yield happens on shutdown); emit ready before yielding
|
|
324
|
+
# Safely derive feature flags from settings which may be models or dicts
|
|
325
|
+
def _get_plugin_enabled(name: str) -> bool:
|
|
326
|
+
plugins_cfg = getattr(settings, "plugins", None)
|
|
327
|
+
if plugins_cfg is None:
|
|
328
|
+
return False
|
|
329
|
+
# dict-like
|
|
330
|
+
if isinstance(plugins_cfg, dict):
|
|
331
|
+
cfg = plugins_cfg.get(name)
|
|
332
|
+
if isinstance(cfg, dict):
|
|
333
|
+
return bool(cfg.get("enabled", False))
|
|
334
|
+
try:
|
|
335
|
+
return bool(getattr(cfg, "enabled", False))
|
|
336
|
+
except Exception:
|
|
337
|
+
return False
|
|
338
|
+
# object-like
|
|
339
|
+
try:
|
|
340
|
+
sub = getattr(plugins_cfg, name, None)
|
|
341
|
+
return bool(getattr(sub, "enabled", False))
|
|
342
|
+
except Exception:
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
def _get_auth_enabled() -> bool:
|
|
346
|
+
auth_cfg = getattr(settings, "auth", None)
|
|
347
|
+
if auth_cfg is None:
|
|
348
|
+
return False
|
|
349
|
+
if isinstance(auth_cfg, dict):
|
|
350
|
+
return bool(auth_cfg.get("enabled", False))
|
|
351
|
+
return bool(getattr(auth_cfg, "enabled", False))
|
|
352
|
+
|
|
353
|
+
logger.info(
|
|
354
|
+
"server_ready",
|
|
355
|
+
url=f"http://{settings.server.host}:{settings.server.port}",
|
|
356
|
+
version=__version__,
|
|
357
|
+
workers=settings.server.workers,
|
|
358
|
+
reload=settings.server.reload,
|
|
359
|
+
features_enabled={
|
|
360
|
+
"plugins": bool(getattr(settings, "enable_plugins", False)),
|
|
361
|
+
"metrics": _get_plugin_enabled("metrics"),
|
|
362
|
+
"access": _get_plugin_enabled("access_log"),
|
|
363
|
+
"auth": _get_auth_enabled(),
|
|
364
|
+
},
|
|
365
|
+
category="lifecycle",
|
|
366
|
+
)
|
|
186
367
|
|
|
187
368
|
yield
|
|
188
369
|
|
|
189
|
-
|
|
190
|
-
logger.debug("server_stop")
|
|
370
|
+
logger.debug("server_stop", category="lifecycle")
|
|
191
371
|
|
|
192
|
-
# Execute shutdown-only components first
|
|
193
372
|
for shutdown_component in SHUTDOWN_ONLY_COMPONENTS:
|
|
194
373
|
if shutdown_component["shutdown"]:
|
|
195
374
|
component_name = shutdown_component["name"]
|
|
196
375
|
try:
|
|
197
|
-
logger.debug(
|
|
376
|
+
logger.debug(
|
|
377
|
+
f"stopping_{component_name.lower().replace(' ', '_')}",
|
|
378
|
+
category="lifecycle",
|
|
379
|
+
)
|
|
198
380
|
await shutdown_component["shutdown"](app)
|
|
381
|
+
except (OSError, PermissionError) as e:
|
|
382
|
+
logger.error(
|
|
383
|
+
f"{component_name.lower().replace(' ', '_')}_shutdown_io_failed",
|
|
384
|
+
error=str(e),
|
|
385
|
+
component=component_name,
|
|
386
|
+
exc_info=e,
|
|
387
|
+
category="lifecycle",
|
|
388
|
+
)
|
|
199
389
|
except Exception as e:
|
|
200
390
|
logger.error(
|
|
201
391
|
f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
|
|
202
392
|
error=str(e),
|
|
203
393
|
component=component_name,
|
|
394
|
+
exc_info=e,
|
|
395
|
+
category="lifecycle",
|
|
204
396
|
)
|
|
205
397
|
|
|
206
|
-
# Execute shutdown components in reverse order
|
|
207
398
|
for component in reversed(LIFECYCLE_COMPONENTS):
|
|
208
399
|
if component["shutdown"]:
|
|
209
400
|
component_name = component["name"]
|
|
210
401
|
try:
|
|
211
|
-
logger.debug(
|
|
212
|
-
|
|
402
|
+
logger.debug(
|
|
403
|
+
f"stopping_{component_name.lower().replace(' ', '_')}",
|
|
404
|
+
category="lifecycle",
|
|
405
|
+
)
|
|
213
406
|
if component_name == "Permission Service":
|
|
214
407
|
await component["shutdown"](app, settings) # type: ignore
|
|
215
408
|
else:
|
|
216
409
|
await component["shutdown"](app) # type: ignore
|
|
410
|
+
except (OSError, PermissionError) as e:
|
|
411
|
+
logger.error(
|
|
412
|
+
f"{component_name.lower().replace(' ', '_')}_shutdown_io_failed",
|
|
413
|
+
error=str(e),
|
|
414
|
+
component=component_name,
|
|
415
|
+
exc_info=e,
|
|
416
|
+
category="lifecycle",
|
|
417
|
+
)
|
|
217
418
|
except Exception as e:
|
|
218
419
|
logger.error(
|
|
219
420
|
f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
|
|
220
421
|
error=str(e),
|
|
221
422
|
component=component_name,
|
|
423
|
+
exc_info=e,
|
|
424
|
+
category="lifecycle",
|
|
222
425
|
)
|
|
223
426
|
|
|
224
427
|
|
|
225
|
-
def create_app(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Returns:
|
|
232
|
-
Configured FastAPI application instance.
|
|
233
|
-
"""
|
|
234
|
-
if settings is None:
|
|
235
|
-
settings = get_settings()
|
|
236
|
-
# Configure logging based on settings BEFORE any module uses logger
|
|
237
|
-
# This is needed for reload mode where the app is re-imported
|
|
238
|
-
|
|
239
|
-
import structlog
|
|
240
|
-
|
|
241
|
-
# Only configure if not already configured or if no file handler exists
|
|
242
|
-
# okay we have the first debug line but after uvicorn start they are not show root_logger = logging.getLogger()
|
|
243
|
-
# for h in root_logger.handlers:
|
|
244
|
-
# print(h)
|
|
245
|
-
# has_file_handler = any(
|
|
246
|
-
# isinstance(h, logging.FileHandler) for h in root_logger.handlers
|
|
247
|
-
# )
|
|
248
|
-
|
|
428
|
+
def create_app(service_container: ServiceContainer | None = None) -> FastAPI:
|
|
429
|
+
if service_container is None:
|
|
430
|
+
service_container = create_service_container()
|
|
431
|
+
"""Create and configure the FastAPI application with plugin system."""
|
|
432
|
+
settings = service_container.get_service(Settings)
|
|
249
433
|
if not structlog.is_configured():
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
json_logs = False
|
|
434
|
+
json_logs = settings.logging.format == "json"
|
|
435
|
+
|
|
253
436
|
setup_logging(
|
|
254
437
|
json_logs=json_logs,
|
|
255
|
-
log_level_name=settings.
|
|
256
|
-
log_file=settings.
|
|
438
|
+
log_level_name=settings.logging.level,
|
|
439
|
+
log_file=settings.logging.file,
|
|
257
440
|
)
|
|
441
|
+
logger.trace("settings", category="lifecycle", settings=settings)
|
|
258
442
|
|
|
259
443
|
app = FastAPI(
|
|
260
444
|
title="CCProxy API Server",
|
|
@@ -263,92 +447,135 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
263
447
|
lifespan=lifespan,
|
|
264
448
|
)
|
|
265
449
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
app.
|
|
450
|
+
app.state.service_container = service_container
|
|
451
|
+
|
|
452
|
+
# Make the FastAPI instance available via the service container for plugin contexts
|
|
453
|
+
service_container.register_service(FastAPI, instance=app)
|
|
454
|
+
|
|
455
|
+
app.state.oauth_registry = service_container.get_oauth_registry()
|
|
456
|
+
|
|
457
|
+
plugin_registry = PluginRegistry()
|
|
458
|
+
middleware_manager = MiddlewareManager()
|
|
459
|
+
|
|
460
|
+
if settings.enable_plugins:
|
|
461
|
+
plugin_registry, middleware_manager = load_plugin_system(settings)
|
|
462
|
+
|
|
463
|
+
# Consolidated plugin init summary at INFO
|
|
464
|
+
logger.info(
|
|
465
|
+
"plugins_initialized",
|
|
466
|
+
plugin_count=len(plugin_registry.factories),
|
|
467
|
+
providers=sum(
|
|
468
|
+
1
|
|
469
|
+
for f in plugin_registry.factories.values()
|
|
470
|
+
if f.get_manifest().is_provider
|
|
471
|
+
),
|
|
472
|
+
system_plugins=len(plugin_registry.factories)
|
|
473
|
+
- sum(
|
|
474
|
+
1
|
|
475
|
+
for f in plugin_registry.factories.values()
|
|
476
|
+
if f.get_manifest().is_provider
|
|
477
|
+
),
|
|
478
|
+
names=list(plugin_registry.factories.keys()),
|
|
479
|
+
category="plugin",
|
|
480
|
+
)
|
|
272
481
|
|
|
273
|
-
|
|
274
|
-
|
|
482
|
+
# Manifest population will be done during startup when core services are available
|
|
483
|
+
|
|
484
|
+
plugin_middleware_count = 0
|
|
485
|
+
for name, factory in plugin_registry.factories.items():
|
|
486
|
+
manifest = factory.get_manifest()
|
|
487
|
+
if manifest.middleware:
|
|
488
|
+
middleware_manager.add_plugin_middleware(name, manifest.middleware)
|
|
489
|
+
plugin_middleware_count += len(manifest.middleware)
|
|
490
|
+
logger.trace(
|
|
491
|
+
"plugin_middleware_collected",
|
|
492
|
+
plugin=name,
|
|
493
|
+
count=len(manifest.middleware),
|
|
494
|
+
category="lifecycle",
|
|
495
|
+
)
|
|
275
496
|
|
|
276
|
-
|
|
277
|
-
|
|
497
|
+
if plugin_middleware_count > 0:
|
|
498
|
+
plugins_with_middleware = [
|
|
499
|
+
n
|
|
500
|
+
for n, f in plugin_registry.factories.items()
|
|
501
|
+
if f.get_manifest().middleware
|
|
502
|
+
]
|
|
503
|
+
logger.debug(
|
|
504
|
+
"plugin_middleware_collection_completed",
|
|
505
|
+
total_middleware=plugin_middleware_count,
|
|
506
|
+
plugins_with_middleware=len(plugins_with_middleware),
|
|
507
|
+
plugin_names=plugins_with_middleware,
|
|
508
|
+
category="lifecycle",
|
|
509
|
+
)
|
|
278
510
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
511
|
+
for name, factory in plugin_registry.factories.items():
|
|
512
|
+
manifest = factory.get_manifest()
|
|
513
|
+
for route_spec in manifest.routes:
|
|
514
|
+
default_tag = name.replace("_", "-")
|
|
515
|
+
# Merge router tags with spec tags, removing duplicates
|
|
516
|
+
merged_tags = merge_router_tags(
|
|
517
|
+
route_spec.router,
|
|
518
|
+
spec_tags=route_spec.tags,
|
|
519
|
+
default_tags=[default_tag],
|
|
520
|
+
)
|
|
282
521
|
|
|
283
|
-
|
|
284
|
-
|
|
522
|
+
app.include_router(
|
|
523
|
+
route_spec.router,
|
|
524
|
+
prefix=route_spec.prefix,
|
|
525
|
+
tags=merged_tags,
|
|
526
|
+
dependencies=route_spec.dependencies,
|
|
527
|
+
)
|
|
528
|
+
logger.debug(
|
|
529
|
+
"plugin_routes_registered",
|
|
530
|
+
plugin=name,
|
|
531
|
+
prefix=route_spec.prefix,
|
|
532
|
+
category="lifecycle",
|
|
533
|
+
)
|
|
285
534
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
app.include_router(prometheus_router, tags=["metrics"])
|
|
535
|
+
app.state.plugin_registry = plugin_registry
|
|
536
|
+
app.state.middleware_manager = middleware_manager
|
|
289
537
|
|
|
290
|
-
|
|
291
|
-
app.include_router(logs_router, prefix="/logs", tags=["logs"])
|
|
538
|
+
app.state.settings = settings
|
|
292
539
|
|
|
293
|
-
|
|
294
|
-
|
|
540
|
+
setup_cors_middleware(app, settings)
|
|
541
|
+
setup_error_handlers(app)
|
|
295
542
|
|
|
296
|
-
|
|
543
|
+
# Validate format adapters once routes are registered
|
|
544
|
+
try:
|
|
545
|
+
registry = service_container.get_format_registry()
|
|
546
|
+
validate_route_format_chains(app=app, registry=registry, logger=logger)
|
|
547
|
+
except Exception as exc:
|
|
548
|
+
# Best-effort registration/validation; do not block app startup
|
|
549
|
+
logger.warning("format_registry_setup_skipped", error=str(exc))
|
|
297
550
|
|
|
298
|
-
|
|
299
|
-
app.include_router(codex_router, tags=["codex"])
|
|
551
|
+
setup_default_middleware(middleware_manager)
|
|
300
552
|
|
|
301
|
-
|
|
302
|
-
app.include_router(claude_router, prefix="/sdk", tags=["claude-sdk"])
|
|
553
|
+
middleware_manager.apply_to_app(app)
|
|
303
554
|
|
|
304
|
-
#
|
|
305
|
-
app.include_router(
|
|
555
|
+
# Core router registrations with tag merging
|
|
556
|
+
app.include_router(
|
|
557
|
+
health_router, tags=merge_router_tags(health_router, default_tags=["health"])
|
|
558
|
+
)
|
|
306
559
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
560
|
+
app.include_router(
|
|
561
|
+
oauth_router,
|
|
562
|
+
prefix="/oauth",
|
|
563
|
+
tags=merge_router_tags(oauth_router, default_tags=["oauth"]),
|
|
564
|
+
)
|
|
310
565
|
|
|
311
|
-
|
|
312
|
-
if settings.claude.builtin_permissions:
|
|
566
|
+
if settings.enable_plugins:
|
|
313
567
|
app.include_router(
|
|
314
|
-
|
|
568
|
+
plugins_router,
|
|
569
|
+
tags=merge_router_tags(plugins_router, default_tags=["plugins"]),
|
|
315
570
|
)
|
|
316
|
-
setup_mcp(app)
|
|
317
|
-
|
|
318
|
-
# Mount static files for dashboard SPA
|
|
319
|
-
from pathlib import Path
|
|
320
|
-
|
|
321
|
-
# Get the path to the dashboard static files
|
|
322
|
-
current_file = Path(__file__)
|
|
323
|
-
project_root = (
|
|
324
|
-
current_file.parent.parent.parent
|
|
325
|
-
) # ccproxy/api/app.py -> project root
|
|
326
|
-
dashboard_static_path = project_root / "ccproxy" / "static" / "dashboard"
|
|
327
|
-
|
|
328
|
-
# Mount dashboard static files if they exist
|
|
329
|
-
if dashboard_static_path.exists():
|
|
330
|
-
# Mount the _app directory for SvelteKit assets at the correct base path
|
|
331
|
-
app_path = dashboard_static_path / "_app"
|
|
332
|
-
if app_path.exists():
|
|
333
|
-
app.mount(
|
|
334
|
-
"/dashboard/_app",
|
|
335
|
-
StaticFiles(directory=str(app_path)),
|
|
336
|
-
name="dashboard-assets",
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
# Mount favicon.svg at root level
|
|
340
|
-
favicon_path = dashboard_static_path / "favicon.svg"
|
|
341
|
-
if favicon_path.exists():
|
|
342
|
-
# For single files, we'll handle this in the dashboard route or add a specific route
|
|
343
|
-
pass
|
|
344
571
|
|
|
345
572
|
return app
|
|
346
573
|
|
|
347
574
|
|
|
348
575
|
def get_app() -> FastAPI:
|
|
349
|
-
"""Get the FastAPI
|
|
576
|
+
"""Get the FastAPI app instance."""
|
|
577
|
+
container = create_service_container()
|
|
578
|
+
return create_app(container)
|
|
350
579
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"""
|
|
354
|
-
return create_app()
|
|
580
|
+
|
|
581
|
+
__all__ = ["create_app", "get_app"]
|