ccproxy-api 0.1.7__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 +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.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.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 -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.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Lightweight CLI discovery for plugin command registration.
|
|
2
|
+
|
|
3
|
+
This module provides minimal plugin discovery specifically for CLI command
|
|
4
|
+
registration, loading only plugin manifests without full initialization.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
from importlib.metadata import entry_points
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
from ccproxy.core.plugins.declaration import PluginManifest
|
|
16
|
+
from ccproxy.core.plugins.discovery import (
|
|
17
|
+
PluginFilter,
|
|
18
|
+
build_combined_plugin_denylist,
|
|
19
|
+
)
|
|
20
|
+
from ccproxy.core.plugins.interfaces import PluginFactory
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = structlog.get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def discover_plugin_cli_extensions(
|
|
27
|
+
settings: Any | None = None,
|
|
28
|
+
) -> list[tuple[str, PluginManifest]]:
|
|
29
|
+
"""Lightweight discovery of plugin CLI extensions.
|
|
30
|
+
|
|
31
|
+
Only loads plugin factories and manifests, no runtime initialization.
|
|
32
|
+
Used during CLI app creation to register plugin commands/arguments.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
settings: Optional settings object to filter plugins
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of (plugin_name, manifest) tuples for plugins with CLI extensions.
|
|
39
|
+
"""
|
|
40
|
+
plugin_manifests = []
|
|
41
|
+
|
|
42
|
+
# Discover from filesystem (plugins/ directory)
|
|
43
|
+
try:
|
|
44
|
+
filesystem_manifests = _discover_filesystem_cli_extensions()
|
|
45
|
+
plugin_manifests.extend(filesystem_manifests)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug("filesystem_cli_discovery_failed", error=str(e))
|
|
48
|
+
|
|
49
|
+
# Discover from entry points
|
|
50
|
+
try:
|
|
51
|
+
entry_point_manifests = _discover_entry_point_cli_extensions()
|
|
52
|
+
plugin_manifests.extend(entry_point_manifests)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.debug("entry_point_cli_discovery_failed", error=str(e))
|
|
55
|
+
|
|
56
|
+
# Remove duplicates (filesystem takes precedence)
|
|
57
|
+
seen_names = set()
|
|
58
|
+
unique_manifests = []
|
|
59
|
+
for name, manifest in plugin_manifests:
|
|
60
|
+
if name not in seen_names:
|
|
61
|
+
unique_manifests.append((name, manifest))
|
|
62
|
+
seen_names.add(name)
|
|
63
|
+
|
|
64
|
+
# Apply plugin filtering if settings provided
|
|
65
|
+
if settings is not None:
|
|
66
|
+
combined_denylist = build_combined_plugin_denylist(
|
|
67
|
+
getattr(settings, "disabled_plugins", None),
|
|
68
|
+
getattr(settings, "plugins", None),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
plugin_filter = PluginFilter(
|
|
72
|
+
enabled_plugins=getattr(settings, "enabled_plugins", None),
|
|
73
|
+
disabled_plugins=combined_denylist,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
filtered_manifests = []
|
|
77
|
+
for name, manifest in unique_manifests:
|
|
78
|
+
if plugin_filter.is_enabled(name):
|
|
79
|
+
filtered_manifests.append((name, manifest))
|
|
80
|
+
else:
|
|
81
|
+
logger.debug(
|
|
82
|
+
"plugin_cli_extension_disabled", plugin=name, category="plugin"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return filtered_manifests
|
|
86
|
+
|
|
87
|
+
return unique_manifests
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _discover_filesystem_cli_extensions() -> list[tuple[str, PluginManifest]]:
|
|
91
|
+
"""Discover CLI extensions from filesystem plugins/ directories."""
|
|
92
|
+
manifests: list[tuple[str, PluginManifest]] = []
|
|
93
|
+
|
|
94
|
+
# Check local plugins/
|
|
95
|
+
plugins_dirs = [
|
|
96
|
+
Path("plugins"),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for plugins_dir in plugins_dirs:
|
|
100
|
+
if not plugins_dir.exists():
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
manifests.extend(_discover_plugins_in_directory(plugins_dir))
|
|
104
|
+
|
|
105
|
+
return manifests
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _discover_plugins_in_directory(
|
|
109
|
+
plugins_dir: Path,
|
|
110
|
+
) -> list[tuple[str, PluginManifest]]:
|
|
111
|
+
"""Discover CLI extensions from a specific plugins directory."""
|
|
112
|
+
manifests: list[tuple[str, PluginManifest]] = []
|
|
113
|
+
|
|
114
|
+
for plugin_path in plugins_dir.iterdir():
|
|
115
|
+
if not plugin_path.is_dir() or plugin_path.name.startswith("_"):
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
plugin_file = plugin_path / "plugin.py"
|
|
119
|
+
if not plugin_file.exists():
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
factory = _load_plugin_factory_from_file(plugin_file)
|
|
124
|
+
if factory:
|
|
125
|
+
manifest = factory.get_manifest()
|
|
126
|
+
if manifest.cli_commands or manifest.cli_arguments:
|
|
127
|
+
manifests.append((manifest.name, manifest))
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.debug(
|
|
130
|
+
"filesystem_plugin_cli_discovery_failed",
|
|
131
|
+
plugin=plugin_path.name,
|
|
132
|
+
error=str(e),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return manifests
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _discover_entry_point_cli_extensions() -> list[tuple[str, PluginManifest]]:
|
|
139
|
+
"""Discover CLI extensions from installed entry points."""
|
|
140
|
+
manifests: list[tuple[str, PluginManifest]] = []
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
plugin_entries = entry_points(group="ccproxy.plugins")
|
|
144
|
+
except Exception:
|
|
145
|
+
return manifests
|
|
146
|
+
|
|
147
|
+
for entry_point in plugin_entries:
|
|
148
|
+
try:
|
|
149
|
+
factory_or_callable = entry_point.load()
|
|
150
|
+
|
|
151
|
+
# Handle both factory instances and factory callables
|
|
152
|
+
if callable(factory_or_callable) and not isinstance(
|
|
153
|
+
factory_or_callable, PluginFactory
|
|
154
|
+
):
|
|
155
|
+
factory = factory_or_callable()
|
|
156
|
+
else:
|
|
157
|
+
factory = factory_or_callable
|
|
158
|
+
|
|
159
|
+
if isinstance(factory, PluginFactory):
|
|
160
|
+
manifest = factory.get_manifest()
|
|
161
|
+
if manifest.cli_commands or manifest.cli_arguments:
|
|
162
|
+
manifests.append((manifest.name, manifest))
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug(
|
|
165
|
+
"entry_point_plugin_cli_discovery_failed",
|
|
166
|
+
entry_point=entry_point.name,
|
|
167
|
+
error=str(e),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return manifests
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _load_plugin_factory_from_file(plugin_file: Path) -> PluginFactory | None:
|
|
174
|
+
"""Load plugin factory from a plugin.py file."""
|
|
175
|
+
try:
|
|
176
|
+
# Use proper package naming for ccproxy plugins
|
|
177
|
+
plugin_name = plugin_file.parent.name
|
|
178
|
+
|
|
179
|
+
# Check if it's in ccproxy/plugins/ structure
|
|
180
|
+
if "ccproxy/plugins" in str(plugin_file):
|
|
181
|
+
module_name = f"ccproxy.plugins.{plugin_name}.plugin"
|
|
182
|
+
else:
|
|
183
|
+
module_name = f"plugin_{plugin_name}"
|
|
184
|
+
|
|
185
|
+
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
|
|
186
|
+
if not spec or not spec.loader:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
module = importlib.util.module_from_spec(spec)
|
|
190
|
+
|
|
191
|
+
# Temporarily add to sys.modules for relative imports
|
|
192
|
+
old_module = sys.modules.get(spec.name)
|
|
193
|
+
sys.modules[spec.name] = module
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
spec.loader.exec_module(module)
|
|
197
|
+
factory = getattr(module, "factory", None)
|
|
198
|
+
|
|
199
|
+
if isinstance(factory, PluginFactory):
|
|
200
|
+
return factory
|
|
201
|
+
finally:
|
|
202
|
+
# Restore original module or remove
|
|
203
|
+
if old_module is not None:
|
|
204
|
+
sys.modules[spec.name] = old_module
|
|
205
|
+
else:
|
|
206
|
+
sys.modules.pop(spec.name, None)
|
|
207
|
+
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
return None
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""Plugin declaration system for static plugin specification.
|
|
2
|
+
|
|
3
|
+
This module provides the declaration layer of the plugin system, allowing plugins
|
|
4
|
+
to specify their requirements and capabilities at declaration time (app creation)
|
|
5
|
+
rather than runtime (lifespan).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import structlog
|
|
15
|
+
from fastapi import APIRouter, FastAPI
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
18
|
+
|
|
19
|
+
from ccproxy.services.adapters.format_adapter import FormatAdapterProtocol
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ccproxy.auth.oauth.registry import OAuthRegistry
|
|
24
|
+
from ccproxy.config.settings import Settings
|
|
25
|
+
from ccproxy.core.plugins import PluginRegistry
|
|
26
|
+
from ccproxy.core.plugins.hooks.base import Hook
|
|
27
|
+
from ccproxy.core.plugins.hooks.manager import HookManager
|
|
28
|
+
from ccproxy.core.plugins.hooks.registry import HookRegistry
|
|
29
|
+
from ccproxy.core.plugins.protocol import OAuthClientProtocol
|
|
30
|
+
from ccproxy.scheduler.core import Scheduler
|
|
31
|
+
from ccproxy.scheduler.tasks import BaseScheduledTask
|
|
32
|
+
from ccproxy.services.adapters.base import BaseAdapter
|
|
33
|
+
from ccproxy.services.cli_detection import CLIDetectionService
|
|
34
|
+
from ccproxy.services.interfaces import (
|
|
35
|
+
IMetricsCollector,
|
|
36
|
+
IRequestTracer,
|
|
37
|
+
StreamingMetrics,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
T = TypeVar("T")
|
|
41
|
+
|
|
42
|
+
# Type aliases for format adapter system
|
|
43
|
+
FormatPair = tuple[str, str]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FormatAdapterSpec:
|
|
48
|
+
"""Specification for format adapter registration."""
|
|
49
|
+
|
|
50
|
+
from_format: str
|
|
51
|
+
to_format: str
|
|
52
|
+
adapter_factory: Callable[
|
|
53
|
+
[], FormatAdapterProtocol | Awaitable[FormatAdapterProtocol]
|
|
54
|
+
]
|
|
55
|
+
priority: int = 100 # Lower = higher priority for conflict resolution
|
|
56
|
+
description: str = ""
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
"""Validate specification."""
|
|
60
|
+
if not self.from_format or not self.to_format:
|
|
61
|
+
raise ValueError("Format names cannot be empty") from None
|
|
62
|
+
if self.from_format == self.to_format:
|
|
63
|
+
raise ValueError("from_format and to_format cannot be the same") from None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def format_pair(self) -> FormatPair:
|
|
67
|
+
"""Get the format pair tuple."""
|
|
68
|
+
return (self.from_format, self.to_format)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MiddlewareLayer(IntEnum):
|
|
72
|
+
"""Middleware layers for ordering."""
|
|
73
|
+
|
|
74
|
+
SECURITY = 100 # Authentication, rate limiting
|
|
75
|
+
OBSERVABILITY = 200 # Logging, metrics
|
|
76
|
+
TRANSFORMATION = 300 # Compression, encoding
|
|
77
|
+
ROUTING = 400 # Path rewriting, proxy
|
|
78
|
+
APPLICATION = 500 # Business logic
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class MiddlewareSpec:
|
|
83
|
+
"""Specification for plugin middleware."""
|
|
84
|
+
|
|
85
|
+
middleware_class: type[BaseHTTPMiddleware]
|
|
86
|
+
priority: int = MiddlewareLayer.APPLICATION
|
|
87
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
def __lt__(self, other: "MiddlewareSpec") -> bool:
|
|
90
|
+
"""Sort by priority (lower values first)."""
|
|
91
|
+
return self.priority < other.priority
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class RouterSpec:
|
|
96
|
+
"""Specification for individual routers in a plugin."""
|
|
97
|
+
|
|
98
|
+
router: APIRouter | Callable[[], APIRouter]
|
|
99
|
+
prefix: str
|
|
100
|
+
tags: list[str] = field(default_factory=list)
|
|
101
|
+
dependencies: list[Any] = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class RouteSpec:
|
|
106
|
+
"""Specification for plugin routes."""
|
|
107
|
+
|
|
108
|
+
router: APIRouter
|
|
109
|
+
prefix: str
|
|
110
|
+
tags: list[str] = field(default_factory=list)
|
|
111
|
+
dependencies: list[Any] = field(default_factory=list)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class TaskSpec:
|
|
116
|
+
"""Specification for scheduled tasks."""
|
|
117
|
+
|
|
118
|
+
task_name: str
|
|
119
|
+
task_type: str
|
|
120
|
+
task_class: type["BaseScheduledTask"] # BaseScheduledTask type from scheduler.tasks
|
|
121
|
+
interval_seconds: float
|
|
122
|
+
enabled: bool = True
|
|
123
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class HookSpec:
|
|
128
|
+
"""Specification for plugin hooks."""
|
|
129
|
+
|
|
130
|
+
hook_class: type["Hook"] # Hook type from hooks.base
|
|
131
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class AuthCommandSpec:
|
|
136
|
+
"""Specification for auth commands."""
|
|
137
|
+
|
|
138
|
+
command_name: str
|
|
139
|
+
description: str
|
|
140
|
+
handler: Callable[..., Any]
|
|
141
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class CliCommandSpec:
|
|
146
|
+
"""Specification for plugin CLI commands."""
|
|
147
|
+
|
|
148
|
+
command_name: str
|
|
149
|
+
command_function: Callable[..., Any]
|
|
150
|
+
help_text: str = ""
|
|
151
|
+
parent_command: str | None = None # For subcommands like "auth login-myservice"
|
|
152
|
+
|
|
153
|
+
def __post_init__(self) -> None:
|
|
154
|
+
"""Validate CLI command specification."""
|
|
155
|
+
if not self.command_name:
|
|
156
|
+
raise ValueError("command_name cannot be empty") from None
|
|
157
|
+
if not callable(self.command_function):
|
|
158
|
+
raise ValueError("command_function must be callable") from None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class CliArgumentSpec:
|
|
163
|
+
"""Specification for adding arguments to existing commands."""
|
|
164
|
+
|
|
165
|
+
target_command: str # e.g., "serve", "auth"
|
|
166
|
+
argument_name: str
|
|
167
|
+
argument_type: type = str
|
|
168
|
+
help_text: str = ""
|
|
169
|
+
default: Any = None
|
|
170
|
+
required: bool = False
|
|
171
|
+
typer_kwargs: dict[str, Any] = field(default_factory=dict)
|
|
172
|
+
|
|
173
|
+
def __post_init__(self) -> None:
|
|
174
|
+
"""Validate CLI argument specification."""
|
|
175
|
+
if not self.target_command:
|
|
176
|
+
raise ValueError("target_command cannot be empty") from None
|
|
177
|
+
if not self.argument_name:
|
|
178
|
+
raise ValueError("argument_name cannot be empty") from None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class PluginManifest:
|
|
183
|
+
"""Complete static declaration of a plugin's capabilities.
|
|
184
|
+
|
|
185
|
+
This manifest is created at module import time and contains all
|
|
186
|
+
static information needed to integrate the plugin into the application.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
# Basic metadata
|
|
190
|
+
name: str
|
|
191
|
+
version: str
|
|
192
|
+
description: str = ""
|
|
193
|
+
dependencies: list[str] = field(default_factory=list)
|
|
194
|
+
|
|
195
|
+
# Plugin type
|
|
196
|
+
is_provider: bool = False # True for provider plugins, False for system plugins
|
|
197
|
+
|
|
198
|
+
# Service declarations
|
|
199
|
+
provides: list[str] = field(default_factory=list) # Services this plugin provides
|
|
200
|
+
requires: list[str] = field(default_factory=list) # Required service dependencies
|
|
201
|
+
optional_requires: list[str] = field(
|
|
202
|
+
default_factory=list
|
|
203
|
+
) # Optional service dependencies
|
|
204
|
+
|
|
205
|
+
# Static specifications
|
|
206
|
+
middleware: list[MiddlewareSpec] = field(default_factory=list)
|
|
207
|
+
routes: list[RouteSpec] = field(default_factory=list)
|
|
208
|
+
tasks: list[TaskSpec] = field(default_factory=list)
|
|
209
|
+
hooks: list[HookSpec] = field(default_factory=list)
|
|
210
|
+
auth_commands: list[AuthCommandSpec] = field(default_factory=list)
|
|
211
|
+
|
|
212
|
+
# Configuration
|
|
213
|
+
config_class: type[BaseModel] | None = None
|
|
214
|
+
tool_accumulator_class: type | None = None
|
|
215
|
+
|
|
216
|
+
# OAuth support (for provider plugins)
|
|
217
|
+
oauth_client_factory: Callable[[], "OAuthClientProtocol"] | None = (
|
|
218
|
+
None # Returns OAuthClientProtocol
|
|
219
|
+
)
|
|
220
|
+
oauth_provider_factory: Callable[[], Any] | None = (
|
|
221
|
+
None # Returns OAuthProviderProtocol
|
|
222
|
+
)
|
|
223
|
+
token_manager_factory: Callable[[], Any] | None = (
|
|
224
|
+
None # Returns TokenManager for the provider
|
|
225
|
+
)
|
|
226
|
+
oauth_config_class: type[BaseModel] | None = None # OAuth configuration model
|
|
227
|
+
oauth_routes: list[RouteSpec] = field(
|
|
228
|
+
default_factory=list
|
|
229
|
+
) # Plugin-specific OAuth routes
|
|
230
|
+
|
|
231
|
+
# Format adapter declarations
|
|
232
|
+
format_adapters: list[FormatAdapterSpec] = field(default_factory=list)
|
|
233
|
+
requires_format_adapters: list[FormatPair] = field(default_factory=list)
|
|
234
|
+
|
|
235
|
+
# CLI extensions
|
|
236
|
+
cli_commands: list[CliCommandSpec] = field(default_factory=list)
|
|
237
|
+
cli_arguments: list[CliArgumentSpec] = field(default_factory=list)
|
|
238
|
+
|
|
239
|
+
def validate_dependencies(self, available_plugins: set[str]) -> list[str]:
|
|
240
|
+
"""Validate that all dependencies are available.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
available_plugins: Set of available plugin names
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of missing dependencies
|
|
247
|
+
"""
|
|
248
|
+
return [dep for dep in self.dependencies if dep not in available_plugins]
|
|
249
|
+
|
|
250
|
+
def validate_service_dependencies(self, available_services: set[str]) -> list[str]:
|
|
251
|
+
"""Validate that required services are available.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
available_services: Set of available service names
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of missing required services
|
|
258
|
+
"""
|
|
259
|
+
missing = []
|
|
260
|
+
for required in self.requires:
|
|
261
|
+
if required not in available_services:
|
|
262
|
+
missing.append(required)
|
|
263
|
+
return missing
|
|
264
|
+
|
|
265
|
+
def get_sorted_middleware(self) -> list[MiddlewareSpec]:
|
|
266
|
+
"""Get middleware sorted by priority."""
|
|
267
|
+
return sorted(self.middleware)
|
|
268
|
+
|
|
269
|
+
def validate_format_adapter_requirements(
|
|
270
|
+
self, available_adapters: set[FormatPair]
|
|
271
|
+
) -> list[FormatPair]:
|
|
272
|
+
"""Validate that required format adapters are available."""
|
|
273
|
+
return [
|
|
274
|
+
req
|
|
275
|
+
for req in self.requires_format_adapters
|
|
276
|
+
if req not in available_adapters
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class PluginContext:
|
|
281
|
+
"""Context provided to plugin runtime during initialization."""
|
|
282
|
+
|
|
283
|
+
def __init__(self) -> None:
|
|
284
|
+
"""Initialize plugin context."""
|
|
285
|
+
# Application settings
|
|
286
|
+
self.settings: Settings | None = None
|
|
287
|
+
self.http_client: httpx.AsyncClient | None = None
|
|
288
|
+
self.logger: structlog.BoundLogger | None = None
|
|
289
|
+
self.scheduler: Scheduler | None = None
|
|
290
|
+
self.config: BaseModel | None = None
|
|
291
|
+
self.cli_detection_service: CLIDetectionService | None = None
|
|
292
|
+
self.plugin_registry: PluginRegistry | None = None
|
|
293
|
+
|
|
294
|
+
# Core app and hook system
|
|
295
|
+
self.app: FastAPI | None = None
|
|
296
|
+
self.hook_registry: HookRegistry | None = None
|
|
297
|
+
self.hook_manager: HookManager | None = None
|
|
298
|
+
|
|
299
|
+
# Observability and streaming
|
|
300
|
+
self.request_tracer: IRequestTracer | None = None
|
|
301
|
+
self.streaming_handler: StreamingMetrics | None = None
|
|
302
|
+
self.metrics: IMetricsCollector | None = None
|
|
303
|
+
|
|
304
|
+
# Provider-specific
|
|
305
|
+
self.adapter: BaseAdapter | None = None
|
|
306
|
+
self.detection_service: Any = None
|
|
307
|
+
self.credentials_manager: Any = None
|
|
308
|
+
self.oauth_registry: OAuthRegistry | None = None
|
|
309
|
+
self.http_pool_manager: Any = None
|
|
310
|
+
self.service_container: Any = None
|
|
311
|
+
self.auth_provider: Any = None
|
|
312
|
+
self.token_manager: Any = None
|
|
313
|
+
self.storage: Any = None
|
|
314
|
+
|
|
315
|
+
self.format_registry: Any = None
|
|
316
|
+
self.model_mapper: Any = None
|
|
317
|
+
|
|
318
|
+
# Testing/utilities
|
|
319
|
+
self.proxy_service: Any = None
|
|
320
|
+
|
|
321
|
+
# Internal service mapping for type-safe access
|
|
322
|
+
self._service_map: dict[type[Any], str] = {}
|
|
323
|
+
self._initialize_service_map()
|
|
324
|
+
|
|
325
|
+
def _initialize_service_map(self) -> None:
|
|
326
|
+
"""Initialize the service type mapping."""
|
|
327
|
+
if TYPE_CHECKING:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
# Map service types to their attribute names
|
|
331
|
+
self._service_map = {
|
|
332
|
+
# Core services - using Any to avoid circular imports at runtime
|
|
333
|
+
**(
|
|
334
|
+
{}
|
|
335
|
+
if TYPE_CHECKING
|
|
336
|
+
else {
|
|
337
|
+
type(None): "settings", # Placeholder, will be populated at runtime
|
|
338
|
+
}
|
|
339
|
+
),
|
|
340
|
+
httpx.AsyncClient: "http_client",
|
|
341
|
+
structlog.BoundLogger: "logger",
|
|
342
|
+
BaseModel: "config",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def get_service(self, service_type: type[T]) -> T:
|
|
346
|
+
"""Get a service instance by type with proper type safety.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
service_type: The type of service to retrieve
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
The service instance
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ValueError: If the service is not available
|
|
356
|
+
"""
|
|
357
|
+
# Create service mappings dynamically to access current values
|
|
358
|
+
service_mappings: dict[type[Any], Any] = {}
|
|
359
|
+
|
|
360
|
+
# Common concrete types
|
|
361
|
+
if self.settings is not None:
|
|
362
|
+
service_mappings[type(self.settings)] = self.settings
|
|
363
|
+
if self.http_client is not None:
|
|
364
|
+
service_mappings[httpx.AsyncClient] = self.http_client
|
|
365
|
+
if self.logger is not None:
|
|
366
|
+
service_mappings[structlog.BoundLogger] = self.logger
|
|
367
|
+
if self.config is not None:
|
|
368
|
+
service_mappings[type(self.config)] = self.config
|
|
369
|
+
service_mappings[BaseModel] = self.config
|
|
370
|
+
|
|
371
|
+
# Check if service type directly matches a known service
|
|
372
|
+
if service_type in service_mappings:
|
|
373
|
+
return service_mappings[service_type] # type: ignore[no-any-return]
|
|
374
|
+
|
|
375
|
+
# Check all attributes for an instance of the requested type
|
|
376
|
+
for attr_name in dir(self):
|
|
377
|
+
if not attr_name.startswith("_"): # Skip private attributes
|
|
378
|
+
attr_value = getattr(self, attr_name)
|
|
379
|
+
if attr_value is not None and isinstance(attr_value, service_type):
|
|
380
|
+
return attr_value # type: ignore[no-any-return]
|
|
381
|
+
|
|
382
|
+
# Service not found
|
|
383
|
+
type_name = getattr(service_type, "__name__", str(service_type))
|
|
384
|
+
raise ValueError(f"Service {type_name} not available in plugin context")
|
|
385
|
+
|
|
386
|
+
def get(self, key_or_type: type[T] | str, default: Any = None) -> T | Any:
|
|
387
|
+
"""Get service by type (new) or by string key (backward compatibility).
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
key_or_type: Service type for type-safe access or string key for compatibility
|
|
391
|
+
default: Default value for string-based access (ignored for type-safe access)
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Service instance for type-safe access, or attribute value for string access
|
|
395
|
+
"""
|
|
396
|
+
if isinstance(key_or_type, str):
|
|
397
|
+
# Backward compatibility: string-based access
|
|
398
|
+
return getattr(self, key_or_type, default)
|
|
399
|
+
else:
|
|
400
|
+
# Type-safe access
|
|
401
|
+
return self.get_service(key_or_type)
|
|
402
|
+
|
|
403
|
+
def get_attr(self, key: str, default: Any = None) -> Any:
|
|
404
|
+
"""Get attribute by string name - for backward compatibility.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
key: String attribute name
|
|
408
|
+
default: Default value if attribute not found
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Attribute value or default
|
|
412
|
+
"""
|
|
413
|
+
return getattr(self, key, default)
|
|
414
|
+
|
|
415
|
+
def __getitem__(self, key: str) -> Any:
|
|
416
|
+
"""Backward compatibility: Allow dictionary-style access."""
|
|
417
|
+
return getattr(self, key, None)
|
|
418
|
+
|
|
419
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
420
|
+
"""Backward compatibility: Allow dictionary-style assignment."""
|
|
421
|
+
setattr(self, key, value)
|
|
422
|
+
|
|
423
|
+
def __contains__(self, key: str) -> bool:
|
|
424
|
+
"""Backward compatibility: Support 'key in context' checks."""
|
|
425
|
+
return hasattr(self, key) and getattr(self, key) is not None
|
|
426
|
+
|
|
427
|
+
def keys(self) -> list[str]:
|
|
428
|
+
"""Backward compatibility: Return list of available service keys."""
|
|
429
|
+
return [
|
|
430
|
+
attr
|
|
431
|
+
for attr in dir(self)
|
|
432
|
+
if not attr.startswith("_")
|
|
433
|
+
and not callable(getattr(self, attr))
|
|
434
|
+
and getattr(self, attr) is not None
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class PluginRuntimeProtocol(Protocol):
|
|
439
|
+
"""Protocol for plugin runtime instances."""
|
|
440
|
+
|
|
441
|
+
async def initialize(self, context: PluginContext) -> None:
|
|
442
|
+
"""Initialize the plugin with runtime context."""
|
|
443
|
+
...
|
|
444
|
+
|
|
445
|
+
async def shutdown(self) -> None:
|
|
446
|
+
"""Cleanup on shutdown."""
|
|
447
|
+
...
|
|
448
|
+
|
|
449
|
+
async def validate(self) -> bool:
|
|
450
|
+
"""Validate plugin is ready."""
|
|
451
|
+
...
|
|
452
|
+
|
|
453
|
+
async def health_check(self) -> dict[str, Any]:
|
|
454
|
+
"""Perform health check."""
|
|
455
|
+
...
|