ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__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 +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- 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 +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- 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 +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- 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 +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.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 -1251
- 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 -243
- ccproxy/services/codex_detection_service.py +0 -252
- 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.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.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.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
ccproxy/api/app.py
CHANGED
|
@@ -1,61 +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
|
-
check_codex_cli_startup,
|
|
38
35
|
check_version_updates_startup,
|
|
39
|
-
flush_streaming_batches_shutdown,
|
|
40
|
-
initialize_claude_detection_startup,
|
|
41
|
-
initialize_claude_sdk_startup,
|
|
42
|
-
initialize_codex_detection_startup,
|
|
43
|
-
initialize_log_storage_shutdown,
|
|
44
|
-
initialize_log_storage_startup,
|
|
45
|
-
initialize_permission_service_startup,
|
|
46
|
-
setup_permission_service_shutdown,
|
|
47
36
|
setup_scheduler_shutdown,
|
|
48
37
|
setup_scheduler_startup,
|
|
49
|
-
setup_session_manager_shutdown,
|
|
50
|
-
validate_claude_authentication_startup,
|
|
51
|
-
validate_codex_authentication_startup,
|
|
52
38
|
)
|
|
53
39
|
|
|
54
40
|
|
|
55
|
-
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
|
|
56
81
|
|
|
57
82
|
|
|
58
|
-
# Type definitions for lifecycle components
|
|
59
83
|
class LifecycleComponent(TypedDict):
|
|
60
84
|
name: str
|
|
61
85
|
startup: Callable[[FastAPI, Any], Awaitable[None]] | None
|
|
@@ -71,47 +95,159 @@ class ShutdownComponent(TypedDict):
|
|
|
71
95
|
shutdown: Callable[[FastAPI], Awaitable[None]] | None
|
|
72
96
|
|
|
73
97
|
|
|
74
|
-
|
|
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
|
+
|
|
75
236
|
LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
|
|
76
237
|
{
|
|
77
|
-
"name": "
|
|
78
|
-
"startup":
|
|
79
|
-
"shutdown":
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"name": "Codex Authentication",
|
|
83
|
-
"startup": validate_codex_authentication_startup,
|
|
84
|
-
"shutdown": None, # One-time validation, no cleanup needed
|
|
238
|
+
"name": "Task Manager",
|
|
239
|
+
"startup": setup_task_manager_startup,
|
|
240
|
+
"shutdown": setup_task_manager_shutdown,
|
|
85
241
|
},
|
|
86
242
|
{
|
|
87
243
|
"name": "Version Check",
|
|
88
244
|
"startup": check_version_updates_startup,
|
|
89
|
-
"shutdown": None,
|
|
245
|
+
"shutdown": None,
|
|
90
246
|
},
|
|
91
247
|
{
|
|
92
248
|
"name": "Claude CLI",
|
|
93
249
|
"startup": check_claude_cli_startup,
|
|
94
|
-
"shutdown": None,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"name": "Codex CLI",
|
|
98
|
-
"startup": check_codex_cli_startup,
|
|
99
|
-
"shutdown": None, # Detection only, no cleanup needed
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
"name": "Claude Detection",
|
|
103
|
-
"startup": initialize_claude_detection_startup,
|
|
104
|
-
"shutdown": None, # No cleanup needed
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"name": "Codex Detection",
|
|
108
|
-
"startup": initialize_codex_detection_startup,
|
|
109
|
-
"shutdown": None, # No cleanup needed
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
"name": "Claude SDK",
|
|
113
|
-
"startup": initialize_claude_sdk_startup,
|
|
114
|
-
"shutdown": setup_session_manager_shutdown,
|
|
250
|
+
"shutdown": None,
|
|
115
251
|
},
|
|
116
252
|
{
|
|
117
253
|
"name": "Scheduler",
|
|
@@ -119,154 +255,190 @@ LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
|
|
|
119
255
|
"shutdown": setup_scheduler_shutdown,
|
|
120
256
|
},
|
|
121
257
|
{
|
|
122
|
-
"name": "
|
|
123
|
-
"startup":
|
|
124
|
-
"shutdown":
|
|
258
|
+
"name": "Service Container",
|
|
259
|
+
"startup": None,
|
|
260
|
+
"shutdown": setup_service_container_shutdown,
|
|
125
261
|
},
|
|
126
262
|
{
|
|
127
|
-
"name": "
|
|
128
|
-
"startup":
|
|
129
|
-
"shutdown":
|
|
263
|
+
"name": "Plugin System",
|
|
264
|
+
"startup": initialize_plugins_startup,
|
|
265
|
+
"shutdown": shutdown_plugins,
|
|
130
266
|
},
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
# Additional shutdown-only components that need special handling
|
|
134
|
-
SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = [
|
|
135
267
|
{
|
|
136
|
-
"name": "
|
|
137
|
-
"
|
|
268
|
+
"name": "Hook System",
|
|
269
|
+
"startup": initialize_hooks_startup,
|
|
270
|
+
"shutdown": shutdown_hook_system,
|
|
138
271
|
},
|
|
139
272
|
]
|
|
140
273
|
|
|
141
|
-
|
|
142
|
-
# Create shared models router
|
|
143
|
-
models_router = APIRouter(tags=["models"])
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@models_router.get("/v1/models", response_model=None)
|
|
147
|
-
async def list_models() -> dict[str, Any]:
|
|
148
|
-
"""List available models.
|
|
149
|
-
|
|
150
|
-
Returns a combined list of Anthropic models and recent OpenAI models.
|
|
151
|
-
This endpoint is shared between both SDK and proxy APIs.
|
|
152
|
-
"""
|
|
153
|
-
return get_models_list()
|
|
274
|
+
SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = []
|
|
154
275
|
|
|
155
276
|
|
|
156
277
|
@asynccontextmanager
|
|
157
278
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
158
279
|
"""Application lifespan manager using component-based approach."""
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# Store settings in app state for reuse in dependencies
|
|
162
|
-
app.state.settings = settings
|
|
163
|
-
|
|
164
|
-
# Startup
|
|
280
|
+
service_container: ServiceContainer = app.state.service_container
|
|
281
|
+
settings = service_container.get_service(Settings)
|
|
165
282
|
logger.info(
|
|
166
|
-
"
|
|
283
|
+
"server_starting",
|
|
167
284
|
host=settings.server.host,
|
|
168
285
|
port=settings.server.port,
|
|
169
286
|
url=f"http://{settings.server.host}:{settings.server.port}",
|
|
287
|
+
category="lifecycle",
|
|
170
288
|
)
|
|
289
|
+
# Demote granular config detail to DEBUG
|
|
171
290
|
logger.debug(
|
|
172
|
-
"server_configured",
|
|
291
|
+
"server_configured",
|
|
292
|
+
host=settings.server.host,
|
|
293
|
+
port=settings.server.port,
|
|
294
|
+
category="config",
|
|
173
295
|
)
|
|
174
296
|
|
|
175
|
-
# Log Claude CLI configuration
|
|
176
|
-
if settings.claude.cli_path:
|
|
177
|
-
logger.debug("claude_cli_configured", cli_path=settings.claude.cli_path)
|
|
178
|
-
else:
|
|
179
|
-
logger.debug("claude_cli_auto_detect")
|
|
180
|
-
logger.debug(
|
|
181
|
-
"claude_cli_search_paths", paths=settings.claude.get_searched_paths()
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Execute startup components in order
|
|
185
297
|
for component in LIFECYCLE_COMPONENTS:
|
|
186
298
|
if component["startup"]:
|
|
187
299
|
component_name = component["name"]
|
|
188
300
|
try:
|
|
189
|
-
logger.debug(
|
|
301
|
+
logger.debug(
|
|
302
|
+
f"starting_{component_name.lower().replace(' ', '_')}",
|
|
303
|
+
category="lifecycle",
|
|
304
|
+
)
|
|
190
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
|
+
)
|
|
191
314
|
except Exception as e:
|
|
192
315
|
logger.error(
|
|
193
316
|
f"{component_name.lower().replace(' ', '_')}_startup_failed",
|
|
194
317
|
error=str(e),
|
|
195
318
|
component=component_name,
|
|
319
|
+
exc_info=e,
|
|
320
|
+
category="lifecycle",
|
|
196
321
|
)
|
|
197
|
-
|
|
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
|
+
)
|
|
198
367
|
|
|
199
368
|
yield
|
|
200
369
|
|
|
201
|
-
|
|
202
|
-
logger.debug("server_stop")
|
|
370
|
+
logger.debug("server_stop", category="lifecycle")
|
|
203
371
|
|
|
204
|
-
# Execute shutdown-only components first
|
|
205
372
|
for shutdown_component in SHUTDOWN_ONLY_COMPONENTS:
|
|
206
373
|
if shutdown_component["shutdown"]:
|
|
207
374
|
component_name = shutdown_component["name"]
|
|
208
375
|
try:
|
|
209
|
-
logger.debug(
|
|
376
|
+
logger.debug(
|
|
377
|
+
f"stopping_{component_name.lower().replace(' ', '_')}",
|
|
378
|
+
category="lifecycle",
|
|
379
|
+
)
|
|
210
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
|
+
)
|
|
211
389
|
except Exception as e:
|
|
212
390
|
logger.error(
|
|
213
391
|
f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
|
|
214
392
|
error=str(e),
|
|
215
393
|
component=component_name,
|
|
394
|
+
exc_info=e,
|
|
395
|
+
category="lifecycle",
|
|
216
396
|
)
|
|
217
397
|
|
|
218
|
-
# Execute shutdown components in reverse order
|
|
219
398
|
for component in reversed(LIFECYCLE_COMPONENTS):
|
|
220
399
|
if component["shutdown"]:
|
|
221
400
|
component_name = component["name"]
|
|
222
401
|
try:
|
|
223
|
-
logger.debug(
|
|
224
|
-
|
|
402
|
+
logger.debug(
|
|
403
|
+
f"stopping_{component_name.lower().replace(' ', '_')}",
|
|
404
|
+
category="lifecycle",
|
|
405
|
+
)
|
|
225
406
|
if component_name == "Permission Service":
|
|
226
407
|
await component["shutdown"](app, settings) # type: ignore
|
|
227
408
|
else:
|
|
228
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
|
+
)
|
|
229
418
|
except Exception as e:
|
|
230
419
|
logger.error(
|
|
231
420
|
f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
|
|
232
421
|
error=str(e),
|
|
233
422
|
component=component_name,
|
|
423
|
+
exc_info=e,
|
|
424
|
+
category="lifecycle",
|
|
234
425
|
)
|
|
235
426
|
|
|
236
427
|
|
|
237
|
-
def create_app(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
Configured FastAPI application instance.
|
|
245
|
-
"""
|
|
246
|
-
if settings is None:
|
|
247
|
-
settings = get_settings()
|
|
248
|
-
# Configure logging based on settings BEFORE any module uses logger
|
|
249
|
-
# This is needed for reload mode where the app is re-imported
|
|
250
|
-
|
|
251
|
-
import structlog
|
|
252
|
-
|
|
253
|
-
# Only configure if not already configured or if no file handler exists
|
|
254
|
-
# okay we have the first debug line but after uvicorn start they are not show root_logger = logging.getLogger()
|
|
255
|
-
# for h in root_logger.handlers:
|
|
256
|
-
# print(h)
|
|
257
|
-
# has_file_handler = any(
|
|
258
|
-
# isinstance(h, logging.FileHandler) for h in root_logger.handlers
|
|
259
|
-
# )
|
|
260
|
-
|
|
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)
|
|
261
433
|
if not structlog.is_configured():
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
json_logs = False
|
|
434
|
+
json_logs = settings.logging.format == "json"
|
|
435
|
+
|
|
265
436
|
setup_logging(
|
|
266
437
|
json_logs=json_logs,
|
|
267
|
-
log_level_name=settings.
|
|
268
|
-
log_file=settings.
|
|
438
|
+
log_level_name=settings.logging.level,
|
|
439
|
+
log_file=settings.logging.file,
|
|
269
440
|
)
|
|
441
|
+
logger.trace("settings", category="lifecycle", settings=settings)
|
|
270
442
|
|
|
271
443
|
app = FastAPI(
|
|
272
444
|
title="CCProxy API Server",
|
|
@@ -275,92 +447,135 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
275
447
|
lifespan=lifespan,
|
|
276
448
|
)
|
|
277
449
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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
|
+
)
|
|
284
481
|
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
)
|
|
287
496
|
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
)
|
|
290
510
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
)
|
|
294
521
|
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
)
|
|
297
534
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
app.include_router(prometheus_router, tags=["metrics"])
|
|
535
|
+
app.state.plugin_registry = plugin_registry
|
|
536
|
+
app.state.middleware_manager = middleware_manager
|
|
301
537
|
|
|
302
|
-
|
|
303
|
-
app.include_router(logs_router, prefix="/logs", tags=["logs"])
|
|
538
|
+
app.state.settings = settings
|
|
304
539
|
|
|
305
|
-
|
|
306
|
-
|
|
540
|
+
setup_cors_middleware(app, settings)
|
|
541
|
+
setup_error_handlers(app)
|
|
307
542
|
|
|
308
|
-
|
|
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))
|
|
309
550
|
|
|
310
|
-
|
|
311
|
-
app.include_router(codex_router, tags=["codex"])
|
|
551
|
+
setup_default_middleware(middleware_manager)
|
|
312
552
|
|
|
313
|
-
|
|
314
|
-
app.include_router(claude_router, prefix="/sdk", tags=["claude-sdk"])
|
|
553
|
+
middleware_manager.apply_to_app(app)
|
|
315
554
|
|
|
316
|
-
#
|
|
317
|
-
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
|
+
)
|
|
318
559
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
560
|
+
app.include_router(
|
|
561
|
+
oauth_router,
|
|
562
|
+
prefix="/oauth",
|
|
563
|
+
tags=merge_router_tags(oauth_router, default_tags=["oauth"]),
|
|
564
|
+
)
|
|
322
565
|
|
|
323
|
-
|
|
324
|
-
if settings.claude.builtin_permissions:
|
|
566
|
+
if settings.enable_plugins:
|
|
325
567
|
app.include_router(
|
|
326
|
-
|
|
568
|
+
plugins_router,
|
|
569
|
+
tags=merge_router_tags(plugins_router, default_tags=["plugins"]),
|
|
327
570
|
)
|
|
328
|
-
setup_mcp(app)
|
|
329
|
-
|
|
330
|
-
# Mount static files for dashboard SPA
|
|
331
|
-
from pathlib import Path
|
|
332
|
-
|
|
333
|
-
# Get the path to the dashboard static files
|
|
334
|
-
current_file = Path(__file__)
|
|
335
|
-
project_root = (
|
|
336
|
-
current_file.parent.parent.parent
|
|
337
|
-
) # ccproxy/api/app.py -> project root
|
|
338
|
-
dashboard_static_path = project_root / "ccproxy" / "static" / "dashboard"
|
|
339
|
-
|
|
340
|
-
# Mount dashboard static files if they exist
|
|
341
|
-
if dashboard_static_path.exists():
|
|
342
|
-
# Mount the _app directory for SvelteKit assets at the correct base path
|
|
343
|
-
app_path = dashboard_static_path / "_app"
|
|
344
|
-
if app_path.exists():
|
|
345
|
-
app.mount(
|
|
346
|
-
"/dashboard/_app",
|
|
347
|
-
StaticFiles(directory=str(app_path)),
|
|
348
|
-
name="dashboard-assets",
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
# Mount favicon.svg at root level
|
|
352
|
-
favicon_path = dashboard_static_path / "favicon.svg"
|
|
353
|
-
if favicon_path.exists():
|
|
354
|
-
# For single files, we'll handle this in the dashboard route or add a specific route
|
|
355
|
-
pass
|
|
356
571
|
|
|
357
572
|
return app
|
|
358
573
|
|
|
359
574
|
|
|
360
575
|
def get_app() -> FastAPI:
|
|
361
|
-
"""Get the FastAPI
|
|
576
|
+
"""Get the FastAPI app instance."""
|
|
577
|
+
container = create_service_container()
|
|
578
|
+
return create_app(container)
|
|
362
579
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
"""
|
|
366
|
-
return create_app()
|
|
580
|
+
|
|
581
|
+
__all__ = ["create_app", "get_app"]
|