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
|
@@ -5,22 +5,23 @@ Provides MCP server functionality including permission checking tools.
|
|
|
5
5
|
|
|
6
6
|
from typing import Annotated
|
|
7
7
|
|
|
8
|
-
from fastapi import FastAPI
|
|
9
|
-
from fastapi_mcp import FastApiMCP
|
|
8
|
+
from fastapi import APIRouter, FastAPI
|
|
9
|
+
from fastapi_mcp import FastApiMCP
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
-
from structlog import get_logger
|
|
12
11
|
|
|
13
12
|
from ccproxy.api.dependencies import SettingsDep
|
|
14
|
-
from ccproxy.
|
|
15
|
-
|
|
16
|
-
from
|
|
13
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
14
|
+
|
|
15
|
+
from .models import (
|
|
16
|
+
PermissionStatus,
|
|
17
17
|
PermissionToolAllowResponse,
|
|
18
18
|
PermissionToolDenyResponse,
|
|
19
19
|
PermissionToolPendingResponse,
|
|
20
20
|
)
|
|
21
|
+
from .service import get_permission_service
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
logger =
|
|
24
|
+
logger = get_plugin_logger()
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class PermissionCheckRequest(BaseModel):
|
|
@@ -125,6 +126,32 @@ async def check_permission(
|
|
|
125
126
|
return PermissionToolDenyResponse(message="Permission request timed out")
|
|
126
127
|
|
|
127
128
|
|
|
129
|
+
# Create a router for the plugin system
|
|
130
|
+
|
|
131
|
+
mcp_router = APIRouter()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp_router.post(
|
|
135
|
+
"/permission/check",
|
|
136
|
+
operation_id="check_permission",
|
|
137
|
+
summary="Check permissions for a tool call",
|
|
138
|
+
description="Validates whether a tool call should be allowed based on security rules",
|
|
139
|
+
response_model=PermissionToolAllowResponse
|
|
140
|
+
| PermissionToolDenyResponse
|
|
141
|
+
| PermissionToolPendingResponse,
|
|
142
|
+
)
|
|
143
|
+
async def permission_endpoint(
|
|
144
|
+
request: PermissionCheckRequest,
|
|
145
|
+
settings: SettingsDep,
|
|
146
|
+
) -> (
|
|
147
|
+
PermissionToolAllowResponse
|
|
148
|
+
| PermissionToolDenyResponse
|
|
149
|
+
| PermissionToolPendingResponse
|
|
150
|
+
):
|
|
151
|
+
"""Check permissions for a tool call."""
|
|
152
|
+
return await check_permission(request, settings)
|
|
153
|
+
|
|
154
|
+
|
|
128
155
|
def setup_mcp(app: FastAPI) -> None:
|
|
129
156
|
"""Set up MCP server on the given FastAPI app.
|
|
130
157
|
|
|
@@ -4,8 +4,9 @@ import asyncio
|
|
|
4
4
|
import uuid
|
|
5
5
|
from datetime import UTC, datetime
|
|
6
6
|
from enum import Enum
|
|
7
|
+
from typing import Annotated, Any, Literal
|
|
7
8
|
|
|
8
|
-
from pydantic import BaseModel, Field, PrivateAttr
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class PermissionStatus(Enum):
|
|
@@ -113,3 +114,66 @@ class PermissionEvent(BaseModel):
|
|
|
113
114
|
resolved_at: str | None = None
|
|
114
115
|
expired_at: str | None = None
|
|
115
116
|
message: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PermissionToolAllowResponse(BaseModel):
|
|
120
|
+
"""Response model for allowed permission tool requests."""
|
|
121
|
+
|
|
122
|
+
behavior: Annotated[Literal["allow"], Field(description="Permission behavior")] = (
|
|
123
|
+
"allow"
|
|
124
|
+
)
|
|
125
|
+
updated_input: Annotated[
|
|
126
|
+
dict[str, Any],
|
|
127
|
+
Field(
|
|
128
|
+
description="Updated input parameters for the tool, or original input if unchanged",
|
|
129
|
+
alias="updatedInput",
|
|
130
|
+
),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class PermissionToolDenyResponse(BaseModel):
|
|
137
|
+
"""Response model for denied permission tool requests."""
|
|
138
|
+
|
|
139
|
+
behavior: Annotated[Literal["deny"], Field(description="Permission behavior")] = (
|
|
140
|
+
"deny"
|
|
141
|
+
)
|
|
142
|
+
message: Annotated[
|
|
143
|
+
str,
|
|
144
|
+
Field(
|
|
145
|
+
description="Human-readable explanation of why the permission was denied"
|
|
146
|
+
),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
model_config = ConfigDict(extra="forbid")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PermissionToolPendingResponse(BaseModel):
|
|
153
|
+
"""Response model for pending permission tool requests requiring user confirmation."""
|
|
154
|
+
|
|
155
|
+
behavior: Annotated[
|
|
156
|
+
Literal["pending"], Field(description="Permission behavior")
|
|
157
|
+
] = "pending"
|
|
158
|
+
confirmation_id: Annotated[
|
|
159
|
+
str,
|
|
160
|
+
Field(
|
|
161
|
+
description="Unique identifier for the confirmation request",
|
|
162
|
+
alias="confirmationId",
|
|
163
|
+
),
|
|
164
|
+
]
|
|
165
|
+
message: Annotated[
|
|
166
|
+
str,
|
|
167
|
+
Field(
|
|
168
|
+
description="Instructions for retrying the request after user confirmation"
|
|
169
|
+
),
|
|
170
|
+
] = "User confirmation required. Please retry with the same confirmation_id."
|
|
171
|
+
|
|
172
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
PermissionToolResponse = (
|
|
176
|
+
PermissionToolAllowResponse
|
|
177
|
+
| PermissionToolDenyResponse
|
|
178
|
+
| PermissionToolPendingResponse
|
|
179
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Permissions plugin v2 implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.core.plugins import (
|
|
7
|
+
PluginContext,
|
|
8
|
+
PluginManifest,
|
|
9
|
+
RouteSpec,
|
|
10
|
+
SystemPluginFactory,
|
|
11
|
+
SystemPluginRuntime,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .config import PermissionsConfig
|
|
15
|
+
from .mcp import mcp_router
|
|
16
|
+
from .routes import router
|
|
17
|
+
from .service import get_permission_service
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = get_plugin_logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PermissionsRuntime(SystemPluginRuntime):
|
|
24
|
+
"""Runtime for permissions plugin."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, manifest: PluginManifest):
|
|
27
|
+
"""Initialize runtime."""
|
|
28
|
+
super().__init__(manifest)
|
|
29
|
+
self.config: PermissionsConfig | None = None
|
|
30
|
+
self.service = get_permission_service()
|
|
31
|
+
|
|
32
|
+
async def _on_initialize(self) -> None:
|
|
33
|
+
"""Initialize the permissions plugin."""
|
|
34
|
+
if not self.context:
|
|
35
|
+
raise RuntimeError("Context not set")
|
|
36
|
+
|
|
37
|
+
# Get configuration
|
|
38
|
+
config = self.context.get("config")
|
|
39
|
+
if not isinstance(config, PermissionsConfig):
|
|
40
|
+
logger.debug("plugin_no_config")
|
|
41
|
+
# Use default config if none provided
|
|
42
|
+
self.config = PermissionsConfig()
|
|
43
|
+
else:
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
logger.debug("initializing_permissions_plugin")
|
|
47
|
+
|
|
48
|
+
# Start the permission service if enabled
|
|
49
|
+
if self.config.enabled:
|
|
50
|
+
# Update service timeout from config
|
|
51
|
+
self.service._timeout_seconds = self.config.timeout_seconds
|
|
52
|
+
await self.service.start()
|
|
53
|
+
logger.debug(
|
|
54
|
+
"permission_service_started",
|
|
55
|
+
timeout_seconds=self.config.timeout_seconds,
|
|
56
|
+
terminal_ui=self.config.enable_terminal_ui,
|
|
57
|
+
sse_stream=self.config.enable_sse_stream,
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
logger.debug("permission_service_disabled")
|
|
61
|
+
|
|
62
|
+
async def _on_shutdown(self) -> None:
|
|
63
|
+
"""Shutdown the plugin and cleanup resources."""
|
|
64
|
+
logger.debug("shutting_down_permissions_plugin")
|
|
65
|
+
|
|
66
|
+
# Stop the permission service
|
|
67
|
+
await self.service.stop()
|
|
68
|
+
|
|
69
|
+
logger.debug("permissions_plugin_shutdown_complete")
|
|
70
|
+
|
|
71
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
72
|
+
"""Get health check details."""
|
|
73
|
+
try:
|
|
74
|
+
# Check if service is running
|
|
75
|
+
pending_count = len(await self.service.get_pending_requests())
|
|
76
|
+
return {
|
|
77
|
+
"type": "system",
|
|
78
|
+
"initialized": self.initialized,
|
|
79
|
+
"pending_requests": pending_count,
|
|
80
|
+
"enabled": self.config.enabled if self.config else False,
|
|
81
|
+
"service_running": self.service is not None,
|
|
82
|
+
}
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error("health_check_failed", error=str(e))
|
|
85
|
+
return {
|
|
86
|
+
"type": "system",
|
|
87
|
+
"initialized": self.initialized,
|
|
88
|
+
"enabled": self.config.enabled if self.config else False,
|
|
89
|
+
"error": str(e),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PermissionsFactory(SystemPluginFactory):
|
|
94
|
+
"""Factory for permissions plugin."""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
"""Initialize factory with manifest."""
|
|
98
|
+
# Create manifest with static declarations
|
|
99
|
+
manifest = PluginManifest(
|
|
100
|
+
name="permissions",
|
|
101
|
+
version="0.1.0",
|
|
102
|
+
description="Permissions plugin providing authorization services for tool calls",
|
|
103
|
+
is_provider=False,
|
|
104
|
+
config_class=PermissionsConfig,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Initialize with manifest
|
|
108
|
+
super().__init__(manifest)
|
|
109
|
+
|
|
110
|
+
def create_runtime(self) -> PermissionsRuntime:
|
|
111
|
+
"""Create runtime instance."""
|
|
112
|
+
return PermissionsRuntime(self.manifest)
|
|
113
|
+
|
|
114
|
+
def create_context(self, core_services: Any) -> PluginContext:
|
|
115
|
+
"""Create context and update manifest with routes if enabled."""
|
|
116
|
+
# Get base context
|
|
117
|
+
context = super().create_context(core_services)
|
|
118
|
+
|
|
119
|
+
# Check if plugin is enabled
|
|
120
|
+
config = context.get("config")
|
|
121
|
+
if isinstance(config, PermissionsConfig) and config.enabled:
|
|
122
|
+
# Add routes to manifest
|
|
123
|
+
# This is safe because it happens during app creation phase
|
|
124
|
+
if not self.manifest.routes:
|
|
125
|
+
self.manifest.routes = []
|
|
126
|
+
|
|
127
|
+
# Always add MCP routes at /mcp root (they're essential for Claude Code)
|
|
128
|
+
mcp_route_spec = RouteSpec(
|
|
129
|
+
router=mcp_router,
|
|
130
|
+
prefix="/mcp",
|
|
131
|
+
tags=["mcp"],
|
|
132
|
+
)
|
|
133
|
+
self.manifest.routes.append(mcp_route_spec)
|
|
134
|
+
|
|
135
|
+
# Add SSE streaming routes at /permissions if enabled
|
|
136
|
+
if config.enable_sse_stream:
|
|
137
|
+
permissions_route_spec = RouteSpec(
|
|
138
|
+
router=router,
|
|
139
|
+
prefix="/permissions",
|
|
140
|
+
tags=["permissions"],
|
|
141
|
+
)
|
|
142
|
+
self.manifest.routes.append(permissions_route_spec)
|
|
143
|
+
|
|
144
|
+
logger.debug(
|
|
145
|
+
"permissions_routes_added_to_manifest",
|
|
146
|
+
sse_enabled=config.enable_sse_stream,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return context
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Export the factory instance
|
|
153
|
+
factory = PermissionsFactory()
|
|
@@ -3,26 +3,31 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
from collections.abc import AsyncGenerator
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
7
8
|
from fastapi import APIRouter, HTTPException, Request
|
|
8
9
|
from pydantic import BaseModel
|
|
9
|
-
from sse_starlette.sse import EventSourceResponse
|
|
10
|
-
from structlog import get_logger
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
from ccproxy.api.dependencies import OptionalSettingsDep
|
|
16
|
+
from ccproxy.auth.dependencies import ConditionalAuthDep
|
|
15
17
|
from ccproxy.core.errors import (
|
|
16
18
|
PermissionAlreadyResolvedError,
|
|
17
19
|
PermissionNotFoundError,
|
|
18
20
|
)
|
|
19
|
-
from ccproxy.
|
|
21
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
20
22
|
|
|
23
|
+
from .models import EventType, PermissionEvent, PermissionStatus
|
|
24
|
+
from .service import get_permission_service
|
|
21
25
|
|
|
22
|
-
logger = get_logger(__name__)
|
|
23
26
|
|
|
27
|
+
logger = get_plugin_logger()
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
router = APIRouter()
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
class PermissionResponse(BaseModel):
|
|
@@ -106,9 +111,9 @@ async def event_generator(
|
|
|
106
111
|
@router.get("/stream")
|
|
107
112
|
async def stream_permissions(
|
|
108
113
|
request: Request,
|
|
109
|
-
settings:
|
|
114
|
+
settings: OptionalSettingsDep,
|
|
110
115
|
auth: ConditionalAuthDep,
|
|
111
|
-
) ->
|
|
116
|
+
) -> Any:
|
|
112
117
|
"""Stream permission requests via Server-Sent Events.
|
|
113
118
|
|
|
114
119
|
This endpoint streams new permission requests as they are created,
|
|
@@ -117,19 +122,18 @@ async def stream_permissions(
|
|
|
117
122
|
Returns:
|
|
118
123
|
EventSourceResponse streaming permission events
|
|
119
124
|
"""
|
|
125
|
+
# Import at runtime to avoid type-checker import requirement
|
|
126
|
+
from sse_starlette.sse import EventSourceResponse
|
|
127
|
+
|
|
120
128
|
return EventSourceResponse(
|
|
121
129
|
event_generator(request),
|
|
122
|
-
headers={
|
|
123
|
-
"Cache-Control": "no-cache",
|
|
124
|
-
"X-Accel-Buffering": "no", # Disable nginx buffering
|
|
125
|
-
},
|
|
126
130
|
)
|
|
127
131
|
|
|
128
132
|
|
|
129
133
|
@router.get("/{permission_id}")
|
|
130
134
|
async def get_permission(
|
|
131
135
|
permission_id: str,
|
|
132
|
-
settings:
|
|
136
|
+
settings: OptionalSettingsDep,
|
|
133
137
|
auth: ConditionalAuthDep,
|
|
134
138
|
) -> PermissionRequestInfo:
|
|
135
139
|
"""Get information about a specific permission request.
|
|
@@ -168,7 +172,7 @@ async def get_permission(
|
|
|
168
172
|
async def respond_to_permission(
|
|
169
173
|
permission_id: str,
|
|
170
174
|
response: PermissionResponse,
|
|
171
|
-
settings:
|
|
175
|
+
settings: OptionalSettingsDep,
|
|
172
176
|
auth: ConditionalAuthDep,
|
|
173
177
|
) -> dict[str, str | bool]:
|
|
174
178
|
"""Submit a response to a permission request.
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
5
|
from datetime import UTC, datetime, timedelta
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from structlog import get_logger
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
9
7
|
|
|
8
|
+
from ccproxy.core.async_task_manager import AsyncTaskManager, create_managed_task
|
|
10
9
|
from ccproxy.core.errors import (
|
|
11
10
|
PermissionNotFoundError,
|
|
12
11
|
)
|
|
13
|
-
from ccproxy.
|
|
12
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
13
|
+
|
|
14
|
+
from .models import (
|
|
14
15
|
EventType,
|
|
15
16
|
PermissionEvent,
|
|
16
17
|
PermissionRequest,
|
|
@@ -18,7 +19,11 @@ from ccproxy.models.permissions import (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ccproxy.services.container import ServiceContainer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = get_plugin_logger()
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
class PermissionService:
|
|
@@ -32,10 +37,38 @@ class PermissionService:
|
|
|
32
37
|
self._event_queues: list[asyncio.Queue[dict[str, Any]]] = []
|
|
33
38
|
self._lock = asyncio.Lock()
|
|
34
39
|
|
|
35
|
-
async def start(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
async def start(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
container: "ServiceContainer | None" = None,
|
|
44
|
+
task_manager: AsyncTaskManager | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
if self._expiry_task is not None:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
self._shutdown = False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
self._expiry_task = await create_managed_task(
|
|
53
|
+
self._expiry_checker(),
|
|
54
|
+
name="permission_expiry_checker",
|
|
55
|
+
creator="PermissionService",
|
|
56
|
+
container=container,
|
|
57
|
+
task_manager=task_manager,
|
|
58
|
+
)
|
|
59
|
+
except RuntimeError as exc:
|
|
60
|
+
if not self._should_fallback_to_unmanaged_task(exc):
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
logger.warning(
|
|
64
|
+
"permission_service_task_manager_unavailable",
|
|
65
|
+
error=str(exc),
|
|
66
|
+
)
|
|
67
|
+
self._expiry_task = asyncio.create_task(
|
|
68
|
+
self._expiry_checker(), name="permission_expiry_checker"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
logger.debug("permission_service_started")
|
|
39
72
|
|
|
40
73
|
async def stop(self) -> None:
|
|
41
74
|
self._shutdown = True
|
|
@@ -179,7 +212,7 @@ class PermissionService:
|
|
|
179
212
|
async def _expiry_checker(self) -> None:
|
|
180
213
|
while not self._shutdown:
|
|
181
214
|
try:
|
|
182
|
-
await asyncio.sleep(
|
|
215
|
+
await asyncio.sleep(self._get_expiry_poll_interval())
|
|
183
216
|
|
|
184
217
|
now = datetime.now(UTC)
|
|
185
218
|
expired_ids = []
|
|
@@ -220,7 +253,7 @@ class PermissionService:
|
|
|
220
253
|
logger.error(
|
|
221
254
|
"expiry_checker_error",
|
|
222
255
|
error=str(e),
|
|
223
|
-
exc_info=
|
|
256
|
+
exc_info=e,
|
|
224
257
|
)
|
|
225
258
|
|
|
226
259
|
def _should_cleanup_request(
|
|
@@ -240,6 +273,27 @@ class PermissionService:
|
|
|
240
273
|
|
|
241
274
|
return False
|
|
242
275
|
|
|
276
|
+
def _get_expiry_poll_interval(self) -> float:
|
|
277
|
+
"""Determine how frequently to poll for expired requests."""
|
|
278
|
+
|
|
279
|
+
timeout = max(self._timeout_seconds, 0)
|
|
280
|
+
if timeout == 0:
|
|
281
|
+
return 0.5
|
|
282
|
+
|
|
283
|
+
return max(0.5, min(5.0, timeout / 2))
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _should_fallback_to_unmanaged_task(exc: RuntimeError) -> bool:
|
|
287
|
+
message = str(exc)
|
|
288
|
+
return any(
|
|
289
|
+
hint in message
|
|
290
|
+
for hint in (
|
|
291
|
+
"Task manager is not started",
|
|
292
|
+
"ServiceContainer is not available",
|
|
293
|
+
"AsyncTaskManager is not registered",
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
243
297
|
async def subscribe_to_events(self) -> asyncio.Queue[dict[str, Any]]:
|
|
244
298
|
"""Subscribe to permission events.
|
|
245
299
|
|
|
@@ -1,23 +1,71 @@
|
|
|
1
1
|
"""Terminal UI handler for confirmation requests using Textual with request stacking support."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import asyncio
|
|
4
6
|
import contextlib
|
|
5
7
|
import time
|
|
6
8
|
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
12
|
+
|
|
13
|
+
from .. import PermissionRequest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# During type checking, import real Textual types; at runtime, provide fallbacks if absent.
|
|
17
|
+
TEXTUAL_AVAILABLE: bool
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from textual.app import App, ComposeResult
|
|
20
|
+
from textual.containers import Container, Vertical
|
|
21
|
+
from textual.events import Key
|
|
22
|
+
from textual.reactive import reactive
|
|
23
|
+
from textual.screen import ModalScreen
|
|
24
|
+
from textual.timer import Timer
|
|
25
|
+
from textual.widgets import Label, Static
|
|
26
|
+
|
|
27
|
+
TEXTUAL_AVAILABLE = True
|
|
28
|
+
else: # pragma: no cover - optional dependency
|
|
29
|
+
try:
|
|
30
|
+
from textual.app import App, ComposeResult
|
|
31
|
+
from textual.containers import Container, Vertical
|
|
32
|
+
from textual.events import Key
|
|
33
|
+
from textual.reactive import reactive
|
|
34
|
+
from textual.screen import ModalScreen
|
|
35
|
+
from textual.timer import Timer
|
|
36
|
+
from textual.widgets import Label, Static
|
|
37
|
+
|
|
38
|
+
TEXTUAL_AVAILABLE = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
TEXTUAL_AVAILABLE = False
|
|
41
|
+
|
|
42
|
+
# Minimal runtime stubs to avoid crashes when Textual is not installed
|
|
43
|
+
class App: # type: ignore[no-redef]
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
class Container: # type: ignore[no-redef]
|
|
47
|
+
pass
|
|
7
48
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from textual.containers import Container, Vertical
|
|
11
|
-
from textual.events import Key
|
|
12
|
-
from textual.reactive import reactive
|
|
13
|
-
from textual.screen import ModalScreen
|
|
14
|
-
from textual.timer import Timer
|
|
15
|
-
from textual.widgets import Label, Static
|
|
49
|
+
class Vertical: # type: ignore[no-redef]
|
|
50
|
+
pass
|
|
16
51
|
|
|
17
|
-
|
|
52
|
+
class ModalScreen: # type: ignore[no-redef]
|
|
53
|
+
pass
|
|
18
54
|
|
|
55
|
+
class Label: # type: ignore[no-redef]
|
|
56
|
+
pass
|
|
19
57
|
|
|
20
|
-
|
|
58
|
+
class Static: # type: ignore[no-redef]
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def reactive(x: float) -> float: # type: ignore[no-redef]
|
|
62
|
+
return x
|
|
63
|
+
|
|
64
|
+
class Timer: # type: ignore[no-redef]
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
logger = get_plugin_logger(__name__)
|
|
21
69
|
|
|
22
70
|
|
|
23
71
|
@dataclass
|
|
@@ -501,6 +549,14 @@ class TerminalPermissionHandler:
|
|
|
501
549
|
Returns:
|
|
502
550
|
bool: True if the user confirmed, False otherwise
|
|
503
551
|
"""
|
|
552
|
+
if not TEXTUAL_AVAILABLE:
|
|
553
|
+
logger.warning(
|
|
554
|
+
"textual_not_available_denying_request",
|
|
555
|
+
request_id=request.id,
|
|
556
|
+
tool_name=request.tool_name,
|
|
557
|
+
)
|
|
558
|
+
return False
|
|
559
|
+
|
|
504
560
|
try:
|
|
505
561
|
logger.info(
|
|
506
562
|
"handling_confirmation_request",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Pricing Plugin
|
|
2
|
+
|
|
3
|
+
Caches model pricing data and exposes it to other plugins for cost awareness.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Loads pricing catalogs and keeps them fresh via the update task
|
|
7
|
+
- Publishes a `pricing` service in the plugin registry for dependents
|
|
8
|
+
- Tracks cache health, age, and failures for health reporting
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `PricingConfig` toggles enablement, refresh cadence, and startup behavior
|
|
12
|
+
- Auto-update schedules can force refresh on launch or run periodically
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin pricing --config-class PricingConfig`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.pricing]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# cache_dir = "~/.cache/ccproxy"
|
|
20
|
+
# cache_ttl_hours = 24
|
|
21
|
+
# source_url = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
|
22
|
+
# download_timeout = 30
|
|
23
|
+
# auto_update = true
|
|
24
|
+
# memory_cache_ttl = 300
|
|
25
|
+
# update_interval_hours = 6.0
|
|
26
|
+
# force_refresh_on_startup = false
|
|
27
|
+
# fallback_to_embedded = false
|
|
28
|
+
# pricing_provider = "all"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Related Components
|
|
32
|
+
- `service.py`: pricing lookup and cache management
|
|
33
|
+
- `tasks.py`: asynchronous cache refresh task
|
|
34
|
+
- `plugin.py`: runtime lifecycle and service registration
|
|
@@ -5,18 +5,19 @@ import time
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
|
-
from structlog import get_logger
|
|
9
8
|
|
|
10
|
-
from ccproxy.
|
|
9
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
10
|
|
|
11
|
+
from .config import PricingConfig
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
logger = get_plugin_logger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class PricingCache:
|
|
17
18
|
"""Manages caching of model pricing data from external sources."""
|
|
18
19
|
|
|
19
|
-
def __init__(self, settings:
|
|
20
|
+
def __init__(self, settings: PricingConfig) -> None:
|
|
20
21
|
"""Initialize pricing cache.
|
|
21
22
|
|
|
22
23
|
Args:
|
|
@@ -84,14 +85,14 @@ class PricingCache:
|
|
|
84
85
|
timeout = self.settings.download_timeout
|
|
85
86
|
|
|
86
87
|
try:
|
|
87
|
-
logger.
|
|
88
|
+
logger.debug("pricing_download_start", url=self.settings.source_url)
|
|
88
89
|
|
|
89
90
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
90
91
|
response = await client.get(self.settings.source_url)
|
|
91
92
|
response.raise_for_status()
|
|
92
93
|
|
|
93
94
|
data = response.json()
|
|
94
|
-
logger.
|
|
95
|
+
logger.debug("pricing_download_completed", model_count=len(data))
|
|
95
96
|
return data # type: ignore[no-any-return]
|
|
96
97
|
|
|
97
98
|
except (httpx.HTTPError, json.JSONDecodeError) as e:
|