ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
"""Plugin factory implementations and registry.
|
|
2
|
+
|
|
3
|
+
This module contains all concrete factory implementations merged from
|
|
4
|
+
base_factory.py and factory.py to eliminate circular dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import structlog
|
|
12
|
+
from fastapi import APIRouter
|
|
13
|
+
|
|
14
|
+
from ccproxy.models.provider import ProviderConfig
|
|
15
|
+
from ccproxy.services.adapters.base import BaseAdapter
|
|
16
|
+
from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
|
|
17
|
+
from ccproxy.services.interfaces import (
|
|
18
|
+
IMetricsCollector,
|
|
19
|
+
IRequestTracer,
|
|
20
|
+
NullMetricsCollector,
|
|
21
|
+
NullRequestTracer,
|
|
22
|
+
NullStreamingHandler,
|
|
23
|
+
StreamingMetrics,
|
|
24
|
+
)
|
|
25
|
+
from ccproxy.utils.model_mapper import ModelMapper
|
|
26
|
+
|
|
27
|
+
from .declaration import (
|
|
28
|
+
CliArgumentSpec,
|
|
29
|
+
CliCommandSpec,
|
|
30
|
+
FormatAdapterSpec,
|
|
31
|
+
FormatPair,
|
|
32
|
+
PluginContext,
|
|
33
|
+
PluginManifest,
|
|
34
|
+
RouterSpec,
|
|
35
|
+
RouteSpec,
|
|
36
|
+
TaskSpec,
|
|
37
|
+
)
|
|
38
|
+
from .interfaces import (
|
|
39
|
+
AuthProviderPluginFactory,
|
|
40
|
+
PluginFactory,
|
|
41
|
+
ProviderPluginFactory,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from ccproxy.config.settings import Settings
|
|
47
|
+
from ccproxy.http.pool import HTTPPoolManager
|
|
48
|
+
from ccproxy.services.container import ServiceContainer
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
logger = structlog.get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
# Type variable for service type checking
|
|
54
|
+
T = Any
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseProviderPluginFactory(ProviderPluginFactory):
|
|
58
|
+
"""Base factory for provider plugins that eliminates common boilerplate.
|
|
59
|
+
|
|
60
|
+
This class uses class attributes for plugin configuration and implements
|
|
61
|
+
common methods that all provider factories share. Subclasses only need
|
|
62
|
+
to define class attributes and override methods that need custom behavior.
|
|
63
|
+
|
|
64
|
+
Required class attributes to be defined by subclasses:
|
|
65
|
+
- plugin_name: str
|
|
66
|
+
- plugin_description: str
|
|
67
|
+
- runtime_class: type[ProviderPluginRuntime]
|
|
68
|
+
- adapter_class: type[BaseAdapter]
|
|
69
|
+
- config_class: type[BaseSettings]
|
|
70
|
+
|
|
71
|
+
Optional class attributes with defaults:
|
|
72
|
+
- plugin_version: str = "1.0.0"
|
|
73
|
+
- detection_service_class: type | None = None
|
|
74
|
+
- credentials_manager_class: type | None = None
|
|
75
|
+
- router: APIRouter | None = None
|
|
76
|
+
- route_prefix: str = "/api"
|
|
77
|
+
- dependencies: list[str] = []
|
|
78
|
+
- optional_requires: list[str] = []
|
|
79
|
+
- tasks: list[TaskSpec] = []
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Required class attributes (must be overridden by subclasses)
|
|
83
|
+
plugin_name: str
|
|
84
|
+
plugin_description: str
|
|
85
|
+
runtime_class: Any # Should be type[ProviderPluginRuntime] subclass
|
|
86
|
+
adapter_class: Any # Should be type[BaseAdapter] subclass
|
|
87
|
+
config_class: Any # Should be type[BaseSettings] subclass
|
|
88
|
+
|
|
89
|
+
# Optional class attributes with defaults
|
|
90
|
+
plugin_version: str = "1.0.0"
|
|
91
|
+
detection_service_class: type | None = None
|
|
92
|
+
credentials_manager_class: type[Any] | None = None
|
|
93
|
+
auth_manager_name: str | None = None # String-based auth manager reference
|
|
94
|
+
routers: list[RouterSpec] = []
|
|
95
|
+
dependencies: list[str] = []
|
|
96
|
+
optional_requires: list[str] = []
|
|
97
|
+
tasks: list[TaskSpec] = []
|
|
98
|
+
|
|
99
|
+
# Format adapter declarations (populated by subclasses)
|
|
100
|
+
format_adapters: list[FormatAdapterSpec] = []
|
|
101
|
+
requires_format_adapters: list[FormatPair] = []
|
|
102
|
+
|
|
103
|
+
# CLI extension declarations (populated by subclasses)
|
|
104
|
+
cli_commands: list[CliCommandSpec] = []
|
|
105
|
+
cli_arguments: list[CliArgumentSpec] = []
|
|
106
|
+
tool_accumulator_class: type | None = None
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
"""Initialize factory with manifest built from class attributes."""
|
|
110
|
+
# Validate required class attributes
|
|
111
|
+
self._validate_class_attributes()
|
|
112
|
+
|
|
113
|
+
# Validate runtime class is a proper subclass
|
|
114
|
+
# Import locally to avoid circular import during module import
|
|
115
|
+
from .runtime import ProviderPluginRuntime
|
|
116
|
+
|
|
117
|
+
if not issubclass(self.runtime_class, ProviderPluginRuntime):
|
|
118
|
+
raise TypeError(
|
|
119
|
+
f"runtime_class {self.runtime_class.__name__} must be a subclass of ProviderPluginRuntime"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Build routes from routers list
|
|
123
|
+
routes = []
|
|
124
|
+
for router_spec in self.routers:
|
|
125
|
+
# Handle both router instances and router factory functions
|
|
126
|
+
router_instance = router_spec.router
|
|
127
|
+
if callable(router_spec.router) and not isinstance(
|
|
128
|
+
router_spec.router, APIRouter
|
|
129
|
+
):
|
|
130
|
+
# Router is a factory function, call it to get the actual router
|
|
131
|
+
router_instance = router_spec.router()
|
|
132
|
+
|
|
133
|
+
routes.append(
|
|
134
|
+
RouteSpec(
|
|
135
|
+
router=cast(APIRouter, router_instance),
|
|
136
|
+
prefix=router_spec.prefix,
|
|
137
|
+
tags=router_spec.tags or [],
|
|
138
|
+
dependencies=router_spec.dependencies,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Create manifest from class attributes
|
|
143
|
+
manifest = PluginManifest(
|
|
144
|
+
name=self.plugin_name,
|
|
145
|
+
version=self.plugin_version,
|
|
146
|
+
description=self.plugin_description,
|
|
147
|
+
is_provider=True,
|
|
148
|
+
config_class=self.config_class,
|
|
149
|
+
tool_accumulator_class=self.tool_accumulator_class,
|
|
150
|
+
dependencies=self.dependencies.copy(),
|
|
151
|
+
optional_requires=self.optional_requires.copy(),
|
|
152
|
+
routes=routes,
|
|
153
|
+
tasks=self.tasks.copy(),
|
|
154
|
+
format_adapters=self.format_adapters.copy(),
|
|
155
|
+
requires_format_adapters=self.requires_format_adapters.copy(),
|
|
156
|
+
cli_commands=self.cli_commands.copy(),
|
|
157
|
+
cli_arguments=self.cli_arguments.copy(),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Format adapter specification validation is deferred to runtime
|
|
161
|
+
# when settings are available via dependency injection
|
|
162
|
+
|
|
163
|
+
# Store the manifest and runtime class directly
|
|
164
|
+
# We don't call parent __init__ because ProviderPluginFactory
|
|
165
|
+
# would override our runtime_class with ProviderPluginRuntime
|
|
166
|
+
self.manifest = manifest
|
|
167
|
+
self.runtime_class = self.__class__.runtime_class
|
|
168
|
+
|
|
169
|
+
def validate_format_adapters_with_settings(self, settings: "Settings") -> None:
|
|
170
|
+
"""Validate format adapter specifications (feature flags removed)."""
|
|
171
|
+
self._validate_format_adapter_specs()
|
|
172
|
+
|
|
173
|
+
def _validate_class_attributes(self) -> None:
|
|
174
|
+
"""Validate that required class attributes are defined."""
|
|
175
|
+
required_attrs = [
|
|
176
|
+
"plugin_name",
|
|
177
|
+
"plugin_description",
|
|
178
|
+
"runtime_class",
|
|
179
|
+
"adapter_class",
|
|
180
|
+
"config_class",
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
for attr in required_attrs:
|
|
184
|
+
if (
|
|
185
|
+
not hasattr(self.__class__, attr)
|
|
186
|
+
or getattr(self.__class__, attr) is None
|
|
187
|
+
):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Class attribute '{attr}' must be defined in {self.__class__.__name__}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _validate_format_adapter_specs(self) -> None:
|
|
193
|
+
"""Validate format adapter specifications."""
|
|
194
|
+
for spec in self.format_adapters:
|
|
195
|
+
if not callable(spec.adapter_factory):
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"Invalid adapter factory for {spec.from_format} -> {spec.to_format}: "
|
|
198
|
+
f"must be callable"
|
|
199
|
+
) from None
|
|
200
|
+
|
|
201
|
+
def create_runtime(self) -> Any:
|
|
202
|
+
"""Create runtime instance using the configured runtime class."""
|
|
203
|
+
return cast(Any, self.runtime_class(self.manifest))
|
|
204
|
+
|
|
205
|
+
async def create_adapter(self, context: PluginContext) -> BaseAdapter:
|
|
206
|
+
"""Create adapter instance with explicit dependencies.
|
|
207
|
+
|
|
208
|
+
This method extracts services from context and creates the adapter
|
|
209
|
+
with explicit dependency injection. Subclasses can override this
|
|
210
|
+
method if they need custom adapter creation logic.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
context: Plugin context
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Adapter instance
|
|
217
|
+
"""
|
|
218
|
+
# Extract services from context (one-time extraction)
|
|
219
|
+
http_pool_manager: HTTPPoolManager | None = cast(
|
|
220
|
+
"HTTPPoolManager | None", context.get("http_pool_manager")
|
|
221
|
+
)
|
|
222
|
+
request_tracer: IRequestTracer | None = context.get("request_tracer")
|
|
223
|
+
metrics: IMetricsCollector | None = context.get("metrics")
|
|
224
|
+
streaming_handler: StreamingMetrics | None = context.get("streaming_handler")
|
|
225
|
+
hook_manager = context.get("hook_manager")
|
|
226
|
+
|
|
227
|
+
# Get auth and detection services that may have been created by factory
|
|
228
|
+
auth_manager = context.get("credentials_manager")
|
|
229
|
+
detection_service = context.get("detection_service")
|
|
230
|
+
|
|
231
|
+
# Get config if available
|
|
232
|
+
config = context.get("config")
|
|
233
|
+
|
|
234
|
+
# Get all adapter dependencies from service container
|
|
235
|
+
service_container = context.get("service_container")
|
|
236
|
+
if not service_container:
|
|
237
|
+
raise RuntimeError("Service container is required for adapter services")
|
|
238
|
+
|
|
239
|
+
# Get standardized adapter dependencies
|
|
240
|
+
adapter_dependencies = service_container.get_adapter_dependencies(metrics)
|
|
241
|
+
|
|
242
|
+
# Check if this is an HTTP-based adapter
|
|
243
|
+
if issubclass(self.adapter_class, BaseHTTPAdapter):
|
|
244
|
+
# HTTP adapters require http_pool_manager
|
|
245
|
+
if not http_pool_manager:
|
|
246
|
+
raise RuntimeError(
|
|
247
|
+
f"HTTP pool manager required for {self.adapter_class.__name__} but not available in context"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Ensure config is provided for HTTP adapters
|
|
251
|
+
if config is None and self.manifest.config_class:
|
|
252
|
+
config = self.manifest.config_class()
|
|
253
|
+
|
|
254
|
+
# Create HTTP adapter with explicit dependencies including format services
|
|
255
|
+
init_params = inspect.signature(self.adapter_class.__init__).parameters
|
|
256
|
+
adapter_kwargs: dict[str, Any] = {
|
|
257
|
+
"config": config,
|
|
258
|
+
"auth_manager": auth_manager,
|
|
259
|
+
"detection_service": detection_service,
|
|
260
|
+
"http_pool_manager": http_pool_manager,
|
|
261
|
+
"request_tracer": request_tracer or NullRequestTracer(),
|
|
262
|
+
"metrics": metrics or NullMetricsCollector(),
|
|
263
|
+
"streaming_handler": streaming_handler or NullStreamingHandler(),
|
|
264
|
+
"hook_manager": hook_manager,
|
|
265
|
+
"format_registry": adapter_dependencies["format_registry"],
|
|
266
|
+
"context": context,
|
|
267
|
+
"model_mapper": context.get("model_mapper")
|
|
268
|
+
if hasattr(context, "get")
|
|
269
|
+
else None,
|
|
270
|
+
}
|
|
271
|
+
if self.tool_accumulator_class:
|
|
272
|
+
adapter_kwargs["tool_accumulator_class"] = self.tool_accumulator_class
|
|
273
|
+
|
|
274
|
+
return cast(BaseAdapter, self.adapter_class(**adapter_kwargs))
|
|
275
|
+
else:
|
|
276
|
+
# Non-HTTP adapters (like ClaudeSDK) have different dependencies
|
|
277
|
+
# Build kwargs based on adapter class constructor signature
|
|
278
|
+
non_http_adapter_kwargs: dict[str, Any] = {}
|
|
279
|
+
|
|
280
|
+
# Get the adapter's __init__ signature
|
|
281
|
+
sig = inspect.signature(self.adapter_class.__init__)
|
|
282
|
+
params = sig.parameters
|
|
283
|
+
|
|
284
|
+
# For non-HTTP adapters, create http_client from pool manager if needed
|
|
285
|
+
client_for_non_http: httpx.AsyncClient | None = None
|
|
286
|
+
if http_pool_manager and "http_client" in params:
|
|
287
|
+
client_for_non_http = await http_pool_manager.get_client()
|
|
288
|
+
|
|
289
|
+
# Map available services to expected parameters
|
|
290
|
+
param_mapping = {
|
|
291
|
+
"config": config,
|
|
292
|
+
"http_client": client_for_non_http,
|
|
293
|
+
"http_pool_manager": http_pool_manager,
|
|
294
|
+
"auth_manager": auth_manager,
|
|
295
|
+
"detection_service": detection_service,
|
|
296
|
+
"session_manager": context.get("session_manager"),
|
|
297
|
+
"request_tracer": request_tracer,
|
|
298
|
+
"metrics": metrics,
|
|
299
|
+
"streaming_handler": streaming_handler,
|
|
300
|
+
"hook_manager": hook_manager,
|
|
301
|
+
"format_registry": adapter_dependencies["format_registry"],
|
|
302
|
+
"context": context,
|
|
303
|
+
"model_mapper": context.get("model_mapper")
|
|
304
|
+
if hasattr(context, "get")
|
|
305
|
+
else None,
|
|
306
|
+
}
|
|
307
|
+
if self.tool_accumulator_class:
|
|
308
|
+
non_http_adapter_kwargs["tool_accumulator_class"] = (
|
|
309
|
+
self.tool_accumulator_class
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Add parameters that the adapter expects
|
|
313
|
+
for param_name, param in params.items():
|
|
314
|
+
if param_name in ("self", "kwargs"):
|
|
315
|
+
continue
|
|
316
|
+
if param_name in param_mapping:
|
|
317
|
+
if param_mapping[param_name] is not None:
|
|
318
|
+
non_http_adapter_kwargs[param_name] = param_mapping[param_name]
|
|
319
|
+
elif (
|
|
320
|
+
param_name == "config"
|
|
321
|
+
and param.default is inspect.Parameter.empty
|
|
322
|
+
and self.manifest.config_class
|
|
323
|
+
):
|
|
324
|
+
# Config is None but required, create default
|
|
325
|
+
default_config = self.manifest.config_class()
|
|
326
|
+
non_http_adapter_kwargs["config"] = default_config
|
|
327
|
+
elif (
|
|
328
|
+
param.default is inspect.Parameter.empty
|
|
329
|
+
and param_name not in non_http_adapter_kwargs
|
|
330
|
+
and param_name == "config"
|
|
331
|
+
and self.manifest.config_class
|
|
332
|
+
):
|
|
333
|
+
# Config parameter is missing but required, create default
|
|
334
|
+
default_config = self.manifest.config_class()
|
|
335
|
+
non_http_adapter_kwargs["config"] = default_config
|
|
336
|
+
|
|
337
|
+
return cast(BaseAdapter, self.adapter_class(**non_http_adapter_kwargs))
|
|
338
|
+
|
|
339
|
+
def create_detection_service(self, context: PluginContext) -> Any:
|
|
340
|
+
"""Create detection service instance if class is configured.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
context: Plugin context
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Detection service instance or None if no class configured
|
|
347
|
+
"""
|
|
348
|
+
if self.detection_service_class is None:
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
settings = context.get("settings")
|
|
352
|
+
if settings is None:
|
|
353
|
+
from ccproxy.config.settings import Settings
|
|
354
|
+
|
|
355
|
+
settings = Settings()
|
|
356
|
+
|
|
357
|
+
cli_service = context.get("cli_detection_service")
|
|
358
|
+
return self.detection_service_class(settings, cli_service)
|
|
359
|
+
|
|
360
|
+
async def create_credentials_manager(self, context: PluginContext) -> Any:
|
|
361
|
+
"""Resolve credentials manager via the shared auth registry."""
|
|
362
|
+
|
|
363
|
+
auth_manager_name = self.get_auth_manager_name(context)
|
|
364
|
+
registry = None
|
|
365
|
+
|
|
366
|
+
service_container = context.get("service_container")
|
|
367
|
+
if service_container and hasattr(
|
|
368
|
+
service_container, "get_auth_manager_registry"
|
|
369
|
+
):
|
|
370
|
+
registry = service_container.get_auth_manager_registry()
|
|
371
|
+
|
|
372
|
+
if not auth_manager_name:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
if not registry:
|
|
376
|
+
logger.warning(
|
|
377
|
+
"auth_manager_registry_unavailable",
|
|
378
|
+
plugin=self.manifest.name,
|
|
379
|
+
auth_manager_name=auth_manager_name,
|
|
380
|
+
category="auth",
|
|
381
|
+
)
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
resolved = await registry.get(auth_manager_name)
|
|
385
|
+
if resolved:
|
|
386
|
+
return resolved
|
|
387
|
+
|
|
388
|
+
# Respect explicit overrides that could not be resolved
|
|
389
|
+
if self.auth_manager_name and auth_manager_name != self.auth_manager_name:
|
|
390
|
+
logger.warning(
|
|
391
|
+
"auth_manager_override_not_resolved",
|
|
392
|
+
plugin=self.manifest.name,
|
|
393
|
+
auth_manager_name=auth_manager_name,
|
|
394
|
+
category="auth",
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
logger.warning(
|
|
398
|
+
"auth_manager_not_registered",
|
|
399
|
+
plugin=self.manifest.name,
|
|
400
|
+
auth_manager_name=auth_manager_name,
|
|
401
|
+
category="auth",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
def get_auth_manager_name(self, context: PluginContext) -> str | None:
|
|
407
|
+
"""Get auth manager name, allowing config override.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
context: Plugin context containing config
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Auth manager name or None if not configured
|
|
414
|
+
"""
|
|
415
|
+
# Check if plugin config overrides auth manager
|
|
416
|
+
if hasattr(context, "config") and context.config:
|
|
417
|
+
config_auth_manager = getattr(context.config, "auth_manager", None)
|
|
418
|
+
if config_auth_manager:
|
|
419
|
+
return str(config_auth_manager)
|
|
420
|
+
|
|
421
|
+
# Use plugin's default auth manager name
|
|
422
|
+
return self.auth_manager_name
|
|
423
|
+
|
|
424
|
+
def create_context(self, service_container: "ServiceContainer") -> PluginContext:
|
|
425
|
+
"""Create context with provider-specific components.
|
|
426
|
+
|
|
427
|
+
This method provides a hook for subclasses to customize context creation.
|
|
428
|
+
The default implementation just returns the base context.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
core_services: Core services container
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Plugin context
|
|
435
|
+
"""
|
|
436
|
+
context = super().create_context(service_container)
|
|
437
|
+
config = context.get("config", None)
|
|
438
|
+
if isinstance(config, ProviderConfig) and config.model_mappings:
|
|
439
|
+
context.model_mapper = ModelMapper(config.model_mappings)
|
|
440
|
+
return context
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class PluginRegistry:
|
|
444
|
+
"""Registry for managing plugin factories and runtime instances."""
|
|
445
|
+
|
|
446
|
+
def __init__(self) -> None:
|
|
447
|
+
"""Initialize plugin registry."""
|
|
448
|
+
self.factories: dict[str, PluginFactory] = {}
|
|
449
|
+
self.runtimes: dict[str, Any] = {}
|
|
450
|
+
self.initialization_order: list[str] = []
|
|
451
|
+
|
|
452
|
+
# Service management
|
|
453
|
+
self._services: dict[str, Any] = {}
|
|
454
|
+
self._service_providers: dict[str, str] = {} # service_name -> plugin_name
|
|
455
|
+
|
|
456
|
+
def register_service(
|
|
457
|
+
self, service_name: str, service_instance: Any, provider_plugin: str
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Register a service provided by a plugin.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
service_name: Name of the service
|
|
463
|
+
service_instance: Service instance
|
|
464
|
+
provider_plugin: Name of the plugin providing the service
|
|
465
|
+
"""
|
|
466
|
+
if service_name in self._services:
|
|
467
|
+
logger.warning(
|
|
468
|
+
"service_already_registered",
|
|
469
|
+
service=service_name,
|
|
470
|
+
existing_provider=self._service_providers[service_name],
|
|
471
|
+
new_provider=provider_plugin,
|
|
472
|
+
)
|
|
473
|
+
self._services[service_name] = service_instance
|
|
474
|
+
self._service_providers[service_name] = provider_plugin
|
|
475
|
+
|
|
476
|
+
def get_service(
|
|
477
|
+
self, service_name: str, service_type: type[T] | None = None
|
|
478
|
+
) -> T | None:
|
|
479
|
+
"""Get a service by name with optional type checking.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
service_name: Name of the service
|
|
483
|
+
service_type: Optional expected service type
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Service instance or None if not found
|
|
487
|
+
"""
|
|
488
|
+
service = self._services.get(service_name)
|
|
489
|
+
if service and service_type and not isinstance(service, service_type):
|
|
490
|
+
logger.warning(
|
|
491
|
+
"service_type_mismatch",
|
|
492
|
+
service=service_name,
|
|
493
|
+
expected_type=service_type,
|
|
494
|
+
actual_type=type(service),
|
|
495
|
+
)
|
|
496
|
+
return None
|
|
497
|
+
return service
|
|
498
|
+
|
|
499
|
+
def has_service(self, service_name: str) -> bool:
|
|
500
|
+
"""Check if a service is registered.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
service_name: Name of the service
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
True if service is registered
|
|
507
|
+
"""
|
|
508
|
+
return service_name in self._services
|
|
509
|
+
|
|
510
|
+
def get_required_services(self, plugin_name: str) -> tuple[list[str], list[str]]:
|
|
511
|
+
"""Get required and optional services for a plugin.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
plugin_name: Name of the plugin
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Tuple of (required_services, optional_services)
|
|
518
|
+
"""
|
|
519
|
+
manifest = self.factories[plugin_name].get_manifest()
|
|
520
|
+
return manifest.requires, manifest.optional_requires
|
|
521
|
+
|
|
522
|
+
def register_factory(self, factory: PluginFactory) -> None:
|
|
523
|
+
"""Register a plugin factory.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
factory: Plugin factory to register
|
|
527
|
+
"""
|
|
528
|
+
manifest = factory.get_manifest()
|
|
529
|
+
|
|
530
|
+
if manifest.name in self.factories:
|
|
531
|
+
raise ValueError(f"Plugin {manifest.name} already registered")
|
|
532
|
+
|
|
533
|
+
self.factories[manifest.name] = factory
|
|
534
|
+
|
|
535
|
+
def get_factory(self, name: str) -> PluginFactory | None:
|
|
536
|
+
"""Get a plugin factory by name.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
name: Plugin name
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Plugin factory or None
|
|
543
|
+
"""
|
|
544
|
+
return self.factories.get(name)
|
|
545
|
+
|
|
546
|
+
def get_all_manifests(self) -> dict[str, PluginManifest]:
|
|
547
|
+
"""Get all registered plugin manifests.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Dictionary mapping plugin names to manifests
|
|
551
|
+
"""
|
|
552
|
+
return {
|
|
553
|
+
name: factory.get_manifest() for name, factory in self.factories.items()
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
def resolve_dependencies(self, settings: "Settings") -> list[str]:
|
|
557
|
+
"""Resolve plugin dependencies and return initialization order.
|
|
558
|
+
|
|
559
|
+
Skips plugins with missing hard dependencies or required services
|
|
560
|
+
instead of failing the entire plugin system. Logs skipped plugins
|
|
561
|
+
and continues with the rest.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
settings: Settings instance
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
List of plugin names in initialization order
|
|
568
|
+
"""
|
|
569
|
+
manifests = self.get_all_manifests()
|
|
570
|
+
|
|
571
|
+
# Start with all plugins available
|
|
572
|
+
available = set(manifests.keys())
|
|
573
|
+
skipped: dict[str, str] = {}
|
|
574
|
+
|
|
575
|
+
# Validate format adapter dependencies (latest behavior)
|
|
576
|
+
missing_format_adapters = self._validate_format_adapter_requirements()
|
|
577
|
+
if missing_format_adapters:
|
|
578
|
+
for plugin_name, missing in missing_format_adapters.items():
|
|
579
|
+
logger.error(
|
|
580
|
+
"plugin_missing_format_adapters",
|
|
581
|
+
plugin=plugin_name,
|
|
582
|
+
missing_adapters=missing,
|
|
583
|
+
category="format",
|
|
584
|
+
)
|
|
585
|
+
# Remove plugins with missing format adapter requirements
|
|
586
|
+
available.discard(plugin_name)
|
|
587
|
+
skipped[plugin_name] = f"missing format adapters: {missing}"
|
|
588
|
+
|
|
589
|
+
# Iteratively prune plugins with unsatisfied dependencies or services
|
|
590
|
+
while True:
|
|
591
|
+
removed_this_pass: set[str] = set()
|
|
592
|
+
|
|
593
|
+
# Compute services provided by currently available plugins
|
|
594
|
+
available_services = {
|
|
595
|
+
service for name in available for service in manifests[name].provides
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for name in sorted(available):
|
|
599
|
+
manifest = manifests[name]
|
|
600
|
+
|
|
601
|
+
# Check plugin dependencies
|
|
602
|
+
missing_plugins = [
|
|
603
|
+
dep for dep in manifest.dependencies if dep not in available
|
|
604
|
+
]
|
|
605
|
+
if missing_plugins:
|
|
606
|
+
removed_this_pass.add(name)
|
|
607
|
+
skipped[name] = f"missing plugin dependencies: {missing_plugins}"
|
|
608
|
+
continue
|
|
609
|
+
|
|
610
|
+
# Check required services
|
|
611
|
+
missing_services = manifest.validate_service_dependencies(
|
|
612
|
+
available_services
|
|
613
|
+
)
|
|
614
|
+
if missing_services:
|
|
615
|
+
removed_this_pass.add(name)
|
|
616
|
+
skipped[name] = f"missing required services: {missing_services}"
|
|
617
|
+
|
|
618
|
+
if not removed_this_pass:
|
|
619
|
+
break
|
|
620
|
+
|
|
621
|
+
# Remove the failing plugins and repeat until stable
|
|
622
|
+
available -= removed_this_pass
|
|
623
|
+
|
|
624
|
+
# Before sorting, ensure provider plugins load before consumers by
|
|
625
|
+
# adding provider plugins to the consumer's dependency list.
|
|
626
|
+
# Choose a stable provider (lexicographically first) when multiple exist.
|
|
627
|
+
for name in available:
|
|
628
|
+
manifest = manifests[name]
|
|
629
|
+
for required_service in manifest.requires:
|
|
630
|
+
provider_names = [
|
|
631
|
+
other_name
|
|
632
|
+
for other_name in available
|
|
633
|
+
if required_service in manifests[other_name].provides
|
|
634
|
+
]
|
|
635
|
+
if provider_names:
|
|
636
|
+
provider_names.sort()
|
|
637
|
+
provider = provider_names[0]
|
|
638
|
+
if provider != name and provider not in manifest.dependencies:
|
|
639
|
+
manifest.dependencies.append(provider)
|
|
640
|
+
|
|
641
|
+
# Kahn's algorithm for topological sort over remaining plugins
|
|
642
|
+
# Build dependency graph restricted to available plugins
|
|
643
|
+
deps: dict[str, list[str]] = {
|
|
644
|
+
name: [dep for dep in manifests[name].dependencies if dep in available]
|
|
645
|
+
for name in available
|
|
646
|
+
}
|
|
647
|
+
in_degree: dict[str, int] = {name: len(deps[name]) for name in available}
|
|
648
|
+
dependents: dict[str, list[str]] = {name: [] for name in available}
|
|
649
|
+
for name, dlist in deps.items():
|
|
650
|
+
for dep in dlist:
|
|
651
|
+
dependents[dep].append(name)
|
|
652
|
+
|
|
653
|
+
# Initialize queue with nodes having zero in-degree
|
|
654
|
+
queue = [name for name, deg in in_degree.items() if deg == 0]
|
|
655
|
+
queue.sort()
|
|
656
|
+
|
|
657
|
+
order: list[str] = []
|
|
658
|
+
while queue:
|
|
659
|
+
node = queue.pop(0)
|
|
660
|
+
order.append(node)
|
|
661
|
+
for consumer in dependents[node]:
|
|
662
|
+
in_degree[consumer] -= 1
|
|
663
|
+
if in_degree[consumer] == 0:
|
|
664
|
+
queue.append(consumer)
|
|
665
|
+
queue.sort()
|
|
666
|
+
|
|
667
|
+
# Any nodes not in order are part of cycles; skip them
|
|
668
|
+
cyclic = [name for name in available if name not in order]
|
|
669
|
+
if cyclic:
|
|
670
|
+
for name in cyclic:
|
|
671
|
+
skipped[name] = "circular dependency"
|
|
672
|
+
logger.error(
|
|
673
|
+
"plugin_dependency_cycle_detected",
|
|
674
|
+
skipped=cyclic,
|
|
675
|
+
category="plugin",
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Final initialization order excludes skipped and cyclic plugins
|
|
679
|
+
self.initialization_order = order
|
|
680
|
+
|
|
681
|
+
if skipped:
|
|
682
|
+
logger.warning(
|
|
683
|
+
"plugins_skipped_due_to_missing_dependencies",
|
|
684
|
+
skipped=skipped,
|
|
685
|
+
category="plugin",
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
return order
|
|
689
|
+
|
|
690
|
+
def _register_auth_manager_with_registry(
|
|
691
|
+
self,
|
|
692
|
+
factory: AuthProviderPluginFactory,
|
|
693
|
+
context: PluginContext,
|
|
694
|
+
) -> None:
|
|
695
|
+
"""Ensure auth provider plugins publish their managers via the registry."""
|
|
696
|
+
|
|
697
|
+
service_container = context.get("service_container")
|
|
698
|
+
if not service_container or not hasattr(
|
|
699
|
+
service_container, "get_auth_manager_registry"
|
|
700
|
+
):
|
|
701
|
+
logger.warning(
|
|
702
|
+
"auth_manager_registry_unavailable",
|
|
703
|
+
plugin=factory.get_manifest().name,
|
|
704
|
+
auth_manager_name=factory.get_auth_manager_registry_name(),
|
|
705
|
+
category="auth",
|
|
706
|
+
)
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
registry = service_container.get_auth_manager_registry()
|
|
710
|
+
manager_name = factory.get_auth_manager_registry_name()
|
|
711
|
+
|
|
712
|
+
if not manager_name:
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
if registry.has(manager_name):
|
|
716
|
+
registry.unregister(manager_name)
|
|
717
|
+
|
|
718
|
+
existing_manager = context.get("token_manager")
|
|
719
|
+
if existing_manager:
|
|
720
|
+
registry.register_instance(manager_name, existing_manager)
|
|
721
|
+
logger.debug(
|
|
722
|
+
"auth_manager_instance_registered",
|
|
723
|
+
plugin=factory.get_manifest().name,
|
|
724
|
+
auth_manager_name=manager_name,
|
|
725
|
+
category="auth",
|
|
726
|
+
)
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
auth_provider = context.get("auth_provider")
|
|
730
|
+
if auth_provider and hasattr(auth_provider, "create_token_manager"):
|
|
731
|
+
|
|
732
|
+
async def manager_factory() -> Any:
|
|
733
|
+
try:
|
|
734
|
+
candidate = auth_provider.create_token_manager()
|
|
735
|
+
return (
|
|
736
|
+
await candidate if inspect.isawaitable(candidate) else candidate
|
|
737
|
+
)
|
|
738
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
739
|
+
logger.error(
|
|
740
|
+
"auth_manager_factory_failed",
|
|
741
|
+
plugin=factory.get_manifest().name,
|
|
742
|
+
auth_manager_name=manager_name,
|
|
743
|
+
error=str(exc),
|
|
744
|
+
exc_info=exc,
|
|
745
|
+
category="auth",
|
|
746
|
+
)
|
|
747
|
+
raise
|
|
748
|
+
|
|
749
|
+
registry.register_factory(manager_name, manager_factory)
|
|
750
|
+
logger.debug(
|
|
751
|
+
"auth_manager_factory_registered",
|
|
752
|
+
plugin=factory.get_manifest().name,
|
|
753
|
+
auth_manager_name=manager_name,
|
|
754
|
+
source="provider",
|
|
755
|
+
category="auth",
|
|
756
|
+
)
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
manager_class = getattr(factory, "auth_manager_class", None)
|
|
760
|
+
if manager_class:
|
|
761
|
+
registry.register_class(manager_name, manager_class)
|
|
762
|
+
logger.debug(
|
|
763
|
+
"auth_manager_class_registered_from_factory",
|
|
764
|
+
plugin=factory.get_manifest().name,
|
|
765
|
+
auth_manager_name=manager_name,
|
|
766
|
+
class_name=manager_class.__name__,
|
|
767
|
+
category="auth",
|
|
768
|
+
)
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
logger.warning(
|
|
772
|
+
"auth_manager_registration_missing",
|
|
773
|
+
plugin=factory.get_manifest().name,
|
|
774
|
+
auth_manager_name=manager_name,
|
|
775
|
+
category="auth",
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
def _validate_format_adapter_requirements(self) -> dict[str, list[tuple[str, str]]]:
|
|
779
|
+
"""Self-contained helper for format adapter requirement validation.
|
|
780
|
+
|
|
781
|
+
This method is called during dependency resolution when core_services
|
|
782
|
+
is not yet available. In practice, format adapter validation happens
|
|
783
|
+
later in the initialization process when the format registry is available.
|
|
784
|
+
"""
|
|
785
|
+
# During dependency resolution phase, format registry may not be available yet
|
|
786
|
+
# Return empty dict to allow dependency resolution to continue
|
|
787
|
+
# Actual format adapter validation happens during initialize_all()
|
|
788
|
+
logger.debug(
|
|
789
|
+
"format_adapter_requirements_validation_deferred",
|
|
790
|
+
message="Format adapter validation will happen during plugin initialization",
|
|
791
|
+
category="format",
|
|
792
|
+
)
|
|
793
|
+
return {}
|
|
794
|
+
|
|
795
|
+
async def create_runtime(
|
|
796
|
+
self, name: str, service_container: "ServiceContainer"
|
|
797
|
+
) -> Any:
|
|
798
|
+
"""Create and initialize a plugin runtime.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
name: Plugin name
|
|
802
|
+
service_container: Service container with all available services
|
|
803
|
+
|
|
804
|
+
Returns:
|
|
805
|
+
Initialized plugin runtime
|
|
806
|
+
|
|
807
|
+
Raises:
|
|
808
|
+
ValueError: If plugin not found
|
|
809
|
+
"""
|
|
810
|
+
factory = self.get_factory(name)
|
|
811
|
+
if not factory:
|
|
812
|
+
raise ValueError(f"Plugin {name} not found")
|
|
813
|
+
|
|
814
|
+
# Check if already created
|
|
815
|
+
if name in self.runtimes:
|
|
816
|
+
return self.runtimes[name]
|
|
817
|
+
|
|
818
|
+
# Create runtime instance
|
|
819
|
+
runtime = factory.create_runtime()
|
|
820
|
+
|
|
821
|
+
# Create context
|
|
822
|
+
context = factory.create_context(service_container)
|
|
823
|
+
|
|
824
|
+
# For auth provider plugins, create auth components first so registries are ready
|
|
825
|
+
if isinstance(factory, AuthProviderPluginFactory):
|
|
826
|
+
context.auth_provider = factory.create_auth_provider(context)
|
|
827
|
+
context.token_manager = factory.create_token_manager()
|
|
828
|
+
context.storage = factory.create_storage()
|
|
829
|
+
self._register_auth_manager_with_registry(factory, context)
|
|
830
|
+
|
|
831
|
+
# For provider plugins, create additional components (may depend on auth registry)
|
|
832
|
+
if isinstance(factory, ProviderPluginFactory):
|
|
833
|
+
# Create credentials manager and detection service first as adapter may depend on them
|
|
834
|
+
context.detection_service = factory.create_detection_service(context)
|
|
835
|
+
context.credentials_manager = await factory.create_credentials_manager(
|
|
836
|
+
context
|
|
837
|
+
)
|
|
838
|
+
context.adapter = await factory.create_adapter(context)
|
|
839
|
+
|
|
840
|
+
# Initialize runtime
|
|
841
|
+
await runtime.initialize(context)
|
|
842
|
+
|
|
843
|
+
# Store runtime
|
|
844
|
+
self.runtimes[name] = runtime
|
|
845
|
+
|
|
846
|
+
return runtime
|
|
847
|
+
|
|
848
|
+
async def initialize_all(self, service_container: "ServiceContainer") -> None:
|
|
849
|
+
"""Initialize all registered plugins with format adapter support.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
service_container: Service container with all available services
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
# Resolve dependencies and get initialization order
|
|
856
|
+
settings = service_container.settings
|
|
857
|
+
order = self.resolve_dependencies(settings)
|
|
858
|
+
|
|
859
|
+
# Consolidated discovery summary at INFO
|
|
860
|
+
logger.info(
|
|
861
|
+
"plugins_discovered", count=len(order), names=order, category="plugin"
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# Register format adapters from manifests in first pass (latest behavior)
|
|
865
|
+
format_registry = service_container.get_format_registry()
|
|
866
|
+
manifests = self.get_all_manifests()
|
|
867
|
+
for name, manifest in manifests.items():
|
|
868
|
+
if manifest.format_adapters:
|
|
869
|
+
await format_registry.register_from_manifest(manifest, name)
|
|
870
|
+
logger.debug(
|
|
871
|
+
"plugin_format_adapters_registered_from_manifest",
|
|
872
|
+
plugin=name,
|
|
873
|
+
adapter_count=len(manifest.format_adapters),
|
|
874
|
+
category="format",
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
# Auth managers are registered when auth provider contexts are constructed
|
|
878
|
+
|
|
879
|
+
initialized: list[str] = []
|
|
880
|
+
for name in order:
|
|
881
|
+
try:
|
|
882
|
+
await self.create_runtime(name, service_container)
|
|
883
|
+
initialized.append(name)
|
|
884
|
+
except Exception as e:
|
|
885
|
+
logger.warning(
|
|
886
|
+
"plugin_initialization_failed",
|
|
887
|
+
plugin=name,
|
|
888
|
+
error=str(e),
|
|
889
|
+
exc_info=e,
|
|
890
|
+
category="plugin",
|
|
891
|
+
)
|
|
892
|
+
# Continue with other plugins
|
|
893
|
+
|
|
894
|
+
# Registry entries are available immediately; log consolidated summary
|
|
895
|
+
skipped = [n for n in order if n not in initialized]
|
|
896
|
+
logger.info(
|
|
897
|
+
"plugins_initialized",
|
|
898
|
+
count=len(initialized),
|
|
899
|
+
names=initialized,
|
|
900
|
+
skipped=skipped if skipped else [],
|
|
901
|
+
category="plugin",
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# Emit a single hooks summary at the end
|
|
905
|
+
try:
|
|
906
|
+
hook_registry = service_container.get_hook_registry()
|
|
907
|
+
totals: dict[str, int] = {}
|
|
908
|
+
for event_name, hooks in hook_registry.list().items():
|
|
909
|
+
totals[event_name] = len(hooks)
|
|
910
|
+
logger.info(
|
|
911
|
+
"hooks_registered",
|
|
912
|
+
total_events=len(totals),
|
|
913
|
+
by_event_counts=totals,
|
|
914
|
+
)
|
|
915
|
+
except Exception:
|
|
916
|
+
pass
|
|
917
|
+
|
|
918
|
+
async def shutdown_all(self) -> None:
|
|
919
|
+
"""Shutdown all plugin runtimes in reverse initialization order."""
|
|
920
|
+
# Shutdown in reverse order
|
|
921
|
+
for name in reversed(self.initialization_order):
|
|
922
|
+
if name in self.runtimes:
|
|
923
|
+
runtime = self.runtimes[name]
|
|
924
|
+
try:
|
|
925
|
+
await runtime.shutdown()
|
|
926
|
+
except Exception as e:
|
|
927
|
+
logger.error(
|
|
928
|
+
"plugin_shutdown_failed",
|
|
929
|
+
plugin=name,
|
|
930
|
+
error=str(e),
|
|
931
|
+
exc_info=e,
|
|
932
|
+
category="plugin",
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# Clear runtimes
|
|
936
|
+
self.runtimes.clear()
|
|
937
|
+
|
|
938
|
+
def get_runtime(self, name: str) -> Any | None:
|
|
939
|
+
"""Get a plugin runtime by name.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
name: Plugin name
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
Plugin runtime or None
|
|
946
|
+
"""
|
|
947
|
+
return self.runtimes.get(name)
|
|
948
|
+
|
|
949
|
+
def list_plugins(self) -> list[str]:
|
|
950
|
+
"""List all registered plugin names.
|
|
951
|
+
|
|
952
|
+
Returns:
|
|
953
|
+
List of plugin names
|
|
954
|
+
"""
|
|
955
|
+
return list(self.factories.keys())
|
|
956
|
+
|
|
957
|
+
def list_provider_plugins(self) -> list[str]:
|
|
958
|
+
"""List all registered provider plugin names.
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
List of provider plugin names
|
|
962
|
+
"""
|
|
963
|
+
return [
|
|
964
|
+
name
|
|
965
|
+
for name, factory in self.factories.items()
|
|
966
|
+
if factory.get_manifest().is_provider
|
|
967
|
+
]
|