ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
ccproxy/config/settings.py
CHANGED
|
@@ -1,49 +1,51 @@
|
|
|
1
|
-
"""Settings configuration for Claude Proxy API Server."""
|
|
2
|
-
|
|
3
|
-
import contextlib
|
|
4
|
-
import json
|
|
5
1
|
import os
|
|
6
2
|
import tomllib
|
|
7
3
|
from pathlib import Path
|
|
8
4
|
from typing import Any
|
|
9
5
|
|
|
10
|
-
import
|
|
11
|
-
from pydantic import Field, field_validator, model_validator
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
12
7
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
8
|
|
|
14
|
-
from ccproxy.
|
|
15
|
-
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
from .
|
|
24
|
-
from .
|
|
25
|
-
from .
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
9
|
+
from ccproxy.core.logging import get_logger
|
|
10
|
+
|
|
11
|
+
from .core import (
|
|
12
|
+
CORSSettings,
|
|
13
|
+
HTTPSettings,
|
|
14
|
+
LoggingSettings,
|
|
15
|
+
PluginDiscoverySettings,
|
|
16
|
+
ServerSettings,
|
|
17
|
+
)
|
|
18
|
+
from .runtime import BinarySettings
|
|
19
|
+
from .security import AuthSettings, SecuritySettings
|
|
20
|
+
from .utils import SchedulerSettings, find_toml_config_file, get_ccproxy_config_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_CONFIG_MISSING_LOGGED = False
|
|
24
|
+
|
|
25
|
+
# Default plugins enabled when no config file exists
|
|
26
|
+
DEFAULT_ENABLED_PLUGINS = [
|
|
27
|
+
"codex",
|
|
28
|
+
"copilot",
|
|
29
|
+
"claude_api",
|
|
30
|
+
"claude_sdk",
|
|
31
|
+
"oauth_codex",
|
|
32
|
+
"oauth_claude",
|
|
35
33
|
]
|
|
36
34
|
|
|
37
35
|
|
|
36
|
+
def _auth_default() -> AuthSettings:
|
|
37
|
+
return AuthSettings(credentials_ttl_seconds=3600.0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["Settings", "ConfigurationError"]
|
|
41
|
+
|
|
42
|
+
|
|
38
43
|
class ConfigurationError(Exception):
|
|
39
44
|
"""Raised when configuration loading or validation fails."""
|
|
40
45
|
|
|
41
46
|
pass
|
|
42
47
|
|
|
43
48
|
|
|
44
|
-
# PoolSettings class removed - connection pooling functionality has been removed
|
|
45
|
-
|
|
46
|
-
|
|
47
49
|
class Settings(BaseSettings):
|
|
48
50
|
"""
|
|
49
51
|
Configuration settings for the Claude Proxy API Server.
|
|
@@ -64,12 +66,16 @@ class Settings(BaseSettings):
|
|
|
64
66
|
env_nested_delimiter="__",
|
|
65
67
|
)
|
|
66
68
|
|
|
67
|
-
# Core application settings
|
|
68
69
|
server: ServerSettings = Field(
|
|
69
70
|
default_factory=ServerSettings,
|
|
70
71
|
description="Server configuration settings",
|
|
71
72
|
)
|
|
72
73
|
|
|
74
|
+
logging: LoggingSettings = Field(
|
|
75
|
+
default_factory=LoggingSettings,
|
|
76
|
+
description="Centralized logging configuration",
|
|
77
|
+
)
|
|
78
|
+
|
|
73
79
|
security: SecuritySettings = Field(
|
|
74
80
|
default_factory=SecuritySettings,
|
|
75
81
|
description="Security configuration settings",
|
|
@@ -80,230 +86,75 @@ class Settings(BaseSettings):
|
|
|
80
86
|
description="CORS configuration settings",
|
|
81
87
|
)
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Codex-specific settings
|
|
90
|
-
codex: CodexSettings = Field(
|
|
91
|
-
default_factory=CodexSettings,
|
|
92
|
-
description="OpenAI Codex-specific configuration settings",
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Proxy and authentication
|
|
96
|
-
reverse_proxy: ReverseProxySettings = Field(
|
|
97
|
-
default_factory=ReverseProxySettings,
|
|
98
|
-
description="Reverse proxy configuration settings",
|
|
89
|
+
http: HTTPSettings = Field(
|
|
90
|
+
default_factory=HTTPSettings,
|
|
91
|
+
description="HTTP client configuration settings",
|
|
92
|
+
json_schema_extra={"config_example_hidden": True},
|
|
99
93
|
)
|
|
100
94
|
|
|
101
95
|
auth: AuthSettings = Field(
|
|
102
|
-
default_factory=
|
|
103
|
-
description="Authentication
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Container settings
|
|
107
|
-
docker: DockerSettings = Field(
|
|
108
|
-
default_factory=DockerSettings,
|
|
109
|
-
description="Docker configuration for running Claude commands in containers",
|
|
96
|
+
default_factory=_auth_default,
|
|
97
|
+
description="Authentication manager settings (e.g., credentials caching)",
|
|
110
98
|
)
|
|
111
99
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
binary: BinarySettings = Field(
|
|
101
|
+
default_factory=BinarySettings,
|
|
102
|
+
description="Binary resolution and package manager fallback configuration",
|
|
103
|
+
json_schema_extra={"config_example_hidden": True},
|
|
116
104
|
)
|
|
117
105
|
|
|
118
|
-
# Scheduler settings
|
|
119
106
|
scheduler: SchedulerSettings = Field(
|
|
120
107
|
default_factory=SchedulerSettings,
|
|
121
108
|
description="Task scheduler configuration settings",
|
|
109
|
+
json_schema_extra={"config_example_hidden": True},
|
|
122
110
|
)
|
|
123
111
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
plugin_discovery: PluginDiscoverySettings = Field(
|
|
113
|
+
default_factory=PluginDiscoverySettings,
|
|
114
|
+
description="Filesystem plugin discovery search paths",
|
|
115
|
+
json_schema_extra={"config_example_hidden": True},
|
|
128
116
|
)
|
|
129
117
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
""
|
|
134
|
-
|
|
135
|
-
return ServerSettings()
|
|
136
|
-
if isinstance(v, ServerSettings):
|
|
137
|
-
return v
|
|
138
|
-
if isinstance(v, dict):
|
|
139
|
-
return ServerSettings(**v)
|
|
140
|
-
return v
|
|
141
|
-
|
|
142
|
-
@field_validator("security", mode="before")
|
|
143
|
-
@classmethod
|
|
144
|
-
def validate_security(cls, v: Any) -> Any:
|
|
145
|
-
"""Validate and convert security settings."""
|
|
146
|
-
if v is None:
|
|
147
|
-
return SecuritySettings()
|
|
148
|
-
if isinstance(v, SecuritySettings):
|
|
149
|
-
return v
|
|
150
|
-
if isinstance(v, dict):
|
|
151
|
-
return SecuritySettings(**v)
|
|
152
|
-
return v
|
|
153
|
-
|
|
154
|
-
@field_validator("cors", mode="before")
|
|
155
|
-
@classmethod
|
|
156
|
-
def validate_cors(cls, v: Any) -> Any:
|
|
157
|
-
"""Validate and convert CORS settings."""
|
|
158
|
-
if v is None:
|
|
159
|
-
return CORSSettings()
|
|
160
|
-
if isinstance(v, CORSSettings):
|
|
161
|
-
return v
|
|
162
|
-
if isinstance(v, dict):
|
|
163
|
-
return CORSSettings(**v)
|
|
164
|
-
return v
|
|
165
|
-
|
|
166
|
-
@field_validator("claude", mode="before")
|
|
167
|
-
@classmethod
|
|
168
|
-
def validate_claude(cls, v: Any) -> Any:
|
|
169
|
-
"""Validate and convert Claude settings."""
|
|
170
|
-
if v is None:
|
|
171
|
-
return ClaudeSettings()
|
|
172
|
-
if isinstance(v, ClaudeSettings):
|
|
173
|
-
return v
|
|
174
|
-
if isinstance(v, dict):
|
|
175
|
-
return ClaudeSettings(**v)
|
|
176
|
-
return v
|
|
177
|
-
|
|
178
|
-
@field_validator("codex", mode="before")
|
|
179
|
-
@classmethod
|
|
180
|
-
def validate_codex(cls, v: Any) -> Any:
|
|
181
|
-
"""Validate and convert Codex settings."""
|
|
182
|
-
if v is None:
|
|
183
|
-
return CodexSettings()
|
|
184
|
-
if isinstance(v, CodexSettings):
|
|
185
|
-
return v
|
|
186
|
-
if isinstance(v, dict):
|
|
187
|
-
return CodexSettings(**v)
|
|
188
|
-
return v
|
|
189
|
-
|
|
190
|
-
@field_validator("reverse_proxy", mode="before")
|
|
191
|
-
@classmethod
|
|
192
|
-
def validate_reverse_proxy(cls, v: Any) -> Any:
|
|
193
|
-
"""Validate and convert reverse proxy settings."""
|
|
194
|
-
if v is None:
|
|
195
|
-
return ReverseProxySettings()
|
|
196
|
-
if isinstance(v, ReverseProxySettings):
|
|
197
|
-
return v
|
|
198
|
-
if isinstance(v, dict):
|
|
199
|
-
return ReverseProxySettings(**v)
|
|
200
|
-
return v
|
|
201
|
-
|
|
202
|
-
@field_validator("auth", mode="before")
|
|
203
|
-
@classmethod
|
|
204
|
-
def validate_auth(cls, v: Any) -> Any:
|
|
205
|
-
"""Validate and convert auth settings."""
|
|
206
|
-
if v is None:
|
|
207
|
-
return AuthSettings()
|
|
208
|
-
if isinstance(v, AuthSettings):
|
|
209
|
-
return v
|
|
210
|
-
if isinstance(v, dict):
|
|
211
|
-
return AuthSettings(**v)
|
|
212
|
-
return v
|
|
213
|
-
|
|
214
|
-
@field_validator("docker", mode="before")
|
|
215
|
-
@classmethod
|
|
216
|
-
def validate_docker_settings(cls, v: Any) -> Any:
|
|
217
|
-
"""Validate and convert Docker settings."""
|
|
218
|
-
if v is None:
|
|
219
|
-
return DockerSettings()
|
|
118
|
+
enable_plugins: bool = Field(
|
|
119
|
+
default=True,
|
|
120
|
+
description="Enable plugin system",
|
|
121
|
+
json_schema_extra={"config_example_hidden": True},
|
|
122
|
+
)
|
|
220
123
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
124
|
+
plugins_disable_local_discovery: bool = Field(
|
|
125
|
+
default=False,
|
|
126
|
+
description=(
|
|
127
|
+
"If true, skip filesystem plugin discovery from the local 'plugins/' directory "
|
|
128
|
+
"and load plugins only from installed entry points."
|
|
129
|
+
),
|
|
130
|
+
json_schema_extra={"config_example_hidden": True},
|
|
131
|
+
)
|
|
224
132
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
133
|
+
enabled_plugins: list[str] | None = Field(
|
|
134
|
+
default=None,
|
|
135
|
+
description="List of explicitly enabled plugins (None = all enabled). Takes precedence over disabled_plugins.",
|
|
136
|
+
json_schema_extra={"config_example_hidden": False},
|
|
137
|
+
)
|
|
228
138
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
139
|
+
disabled_plugins: list[str] | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="List of explicitly disabled plugins.",
|
|
142
|
+
json_schema_extra={"config_example_hidden": True},
|
|
143
|
+
)
|
|
234
144
|
|
|
235
|
-
|
|
145
|
+
# CLI context for plugin access (set dynamically)
|
|
146
|
+
cli_context: dict[str, Any] = Field(default_factory=dict, exclude=True)
|
|
236
147
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if v is None:
|
|
242
|
-
return ObservabilitySettings()
|
|
243
|
-
if isinstance(v, ObservabilitySettings):
|
|
244
|
-
return v
|
|
245
|
-
if isinstance(v, dict):
|
|
246
|
-
return ObservabilitySettings(**v)
|
|
247
|
-
return v
|
|
248
|
-
|
|
249
|
-
@field_validator("scheduler", mode="before")
|
|
250
|
-
@classmethod
|
|
251
|
-
def validate_scheduler(cls, v: Any) -> Any:
|
|
252
|
-
"""Validate and convert scheduler settings."""
|
|
253
|
-
if v is None:
|
|
254
|
-
return SchedulerSettings()
|
|
255
|
-
if isinstance(v, SchedulerSettings):
|
|
256
|
-
return v
|
|
257
|
-
if isinstance(v, dict):
|
|
258
|
-
return SchedulerSettings(**v)
|
|
259
|
-
return v
|
|
260
|
-
|
|
261
|
-
@field_validator("pricing", mode="before")
|
|
262
|
-
@classmethod
|
|
263
|
-
def validate_pricing(cls, v: Any) -> Any:
|
|
264
|
-
"""Validate and convert pricing settings."""
|
|
265
|
-
if v is None:
|
|
266
|
-
return PricingSettings()
|
|
267
|
-
if isinstance(v, PricingSettings):
|
|
268
|
-
return v
|
|
269
|
-
if isinstance(v, dict):
|
|
270
|
-
return PricingSettings(**v)
|
|
271
|
-
return v
|
|
272
|
-
|
|
273
|
-
# validate_pool_settings method removed - connection pooling functionality has been removed
|
|
148
|
+
plugins: dict[str, dict[str, Any]] = Field(
|
|
149
|
+
default_factory=dict,
|
|
150
|
+
description="Plugin-specific configurations keyed by plugin name",
|
|
151
|
+
)
|
|
274
152
|
|
|
275
153
|
@property
|
|
276
154
|
def server_url(self) -> str:
|
|
277
155
|
"""Get the complete server URL."""
|
|
278
156
|
return f"http://{self.server.host}:{self.server.port}"
|
|
279
157
|
|
|
280
|
-
@property
|
|
281
|
-
def is_development(self) -> bool:
|
|
282
|
-
"""Check if running in development mode."""
|
|
283
|
-
return self.server.reload or self.server.log_level == "DEBUG"
|
|
284
|
-
|
|
285
|
-
@model_validator(mode="after")
|
|
286
|
-
def setup_claude_cli_path(self) -> "Settings":
|
|
287
|
-
"""Set up Claude CLI path in environment if provided or found."""
|
|
288
|
-
# If not explicitly set, try to find it
|
|
289
|
-
if not self.claude.cli_path:
|
|
290
|
-
found_path, found_in_path = self.claude.find_claude_cli()
|
|
291
|
-
if found_path:
|
|
292
|
-
self.claude.cli_path = found_path
|
|
293
|
-
# Only add to PATH if it wasn't found via which()
|
|
294
|
-
if not found_in_path:
|
|
295
|
-
cli_dir = str(Path(self.claude.cli_path).parent)
|
|
296
|
-
current_path = os.environ.get("PATH", "")
|
|
297
|
-
if cli_dir not in current_path:
|
|
298
|
-
os.environ["PATH"] = f"{cli_dir}:{current_path}"
|
|
299
|
-
elif self.claude.cli_path:
|
|
300
|
-
# If explicitly set, always add to PATH
|
|
301
|
-
cli_dir = str(Path(self.claude.cli_path).parent)
|
|
302
|
-
current_path = os.environ.get("PATH", "")
|
|
303
|
-
if cli_dir not in current_path:
|
|
304
|
-
os.environ["PATH"] = f"{cli_dir}:{current_path}"
|
|
305
|
-
return self
|
|
306
|
-
|
|
307
158
|
def model_dump_safe(self) -> dict[str, Any]:
|
|
308
159
|
"""
|
|
309
160
|
Dump model data with sensitive information masked.
|
|
@@ -311,21 +162,61 @@ class Settings(BaseSettings):
|
|
|
311
162
|
Returns:
|
|
312
163
|
dict: Configuration with sensitive data masked
|
|
313
164
|
"""
|
|
314
|
-
return self.model_dump()
|
|
165
|
+
return self.model_dump(mode="json")
|
|
315
166
|
|
|
316
167
|
@classmethod
|
|
317
|
-
def
|
|
318
|
-
"""
|
|
168
|
+
def _validate_deprecated_keys(cls, config_data: dict[str, Any]) -> None:
|
|
169
|
+
"""Fail fast if deprecated legacy config keys are present."""
|
|
170
|
+
deprecated_hits: list[tuple[str, str]] = []
|
|
171
|
+
|
|
172
|
+
scheduler_cfg = config_data.get("scheduler") or {}
|
|
173
|
+
if isinstance(scheduler_cfg, dict):
|
|
174
|
+
key_map = {
|
|
175
|
+
"pushgateway_enabled": "plugins.metrics.pushgateway_enabled",
|
|
176
|
+
"pushgateway_url": "plugins.metrics.pushgateway_url",
|
|
177
|
+
"pushgateway_job": "plugins.metrics.pushgateway_job",
|
|
178
|
+
"pushgateway_interval_seconds": "plugins.metrics.pushgateway_push_interval",
|
|
179
|
+
}
|
|
180
|
+
for old_key, new_key in key_map.items():
|
|
181
|
+
if old_key in scheduler_cfg:
|
|
182
|
+
deprecated_hits.append((f"scheduler.{old_key}", new_key))
|
|
319
183
|
|
|
320
|
-
|
|
321
|
-
|
|
184
|
+
if "observability" in config_data:
|
|
185
|
+
deprecated_hits.append(
|
|
186
|
+
("observability.*", "plugins.* (metrics/analytics/dashboard)")
|
|
187
|
+
)
|
|
322
188
|
|
|
323
|
-
|
|
324
|
-
|
|
189
|
+
for env_key in os.environ:
|
|
190
|
+
upper = env_key.upper()
|
|
191
|
+
if upper.startswith("SCHEDULER__PUSHGATEWAY_"):
|
|
192
|
+
env_map = {
|
|
193
|
+
"SCHEDULER__PUSHGATEWAY_ENABLED": "plugins.metrics.pushgateway_enabled",
|
|
194
|
+
"SCHEDULER__PUSHGATEWAY_URL": "plugins.metrics.pushgateway_url",
|
|
195
|
+
"SCHEDULER__PUSHGATEWAY_JOB": "plugins.metrics.pushgateway_job",
|
|
196
|
+
"SCHEDULER__PUSHGATEWAY_INTERVAL_SECONDS": "plugins.metrics.pushgateway_push_interval",
|
|
197
|
+
}
|
|
198
|
+
target = env_map.get(upper, "plugins.metrics.*")
|
|
199
|
+
deprecated_hits.append((env_key, target))
|
|
200
|
+
if upper.startswith("OBSERVABILITY__"):
|
|
201
|
+
deprecated_hits.append(
|
|
202
|
+
(env_key, "plugins.* (metrics/analytics/dashboard)")
|
|
203
|
+
)
|
|
325
204
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
205
|
+
if deprecated_hits:
|
|
206
|
+
lines = [
|
|
207
|
+
"Removed configuration keys detected. The following are no longer supported:",
|
|
208
|
+
]
|
|
209
|
+
for old, new in deprecated_hits:
|
|
210
|
+
lines.append(f"- {old} → {new}")
|
|
211
|
+
lines.append(
|
|
212
|
+
"Configure corresponding plugin settings under [plugins.*]. "
|
|
213
|
+
"See: ccproxy/plugins/metrics/README.md and the Plugin Config Quickstart."
|
|
214
|
+
)
|
|
215
|
+
raise ValueError("\n".join(lines))
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def load_toml_config(cls, toml_path: Path) -> dict[str, Any]:
|
|
219
|
+
"""Load configuration from a TOML file."""
|
|
329
220
|
try:
|
|
330
221
|
with toml_path.open("rb") as f:
|
|
331
222
|
return tomllib.load(f)
|
|
@@ -336,17 +227,7 @@ class Settings(BaseSettings):
|
|
|
336
227
|
|
|
337
228
|
@classmethod
|
|
338
229
|
def load_config_file(cls, config_path: Path) -> dict[str, Any]:
|
|
339
|
-
"""Load configuration from a file based on its extension.
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
config_path: Path to the configuration file
|
|
343
|
-
|
|
344
|
-
Returns:
|
|
345
|
-
dict: Configuration data from the file
|
|
346
|
-
|
|
347
|
-
Raises:
|
|
348
|
-
ValueError: If the file format is unsupported or invalid
|
|
349
|
-
"""
|
|
230
|
+
"""Load configuration from a file based on its extension."""
|
|
350
231
|
suffix = config_path.suffix.lower()
|
|
351
232
|
|
|
352
233
|
if suffix in [".toml"]:
|
|
@@ -359,219 +240,279 @@ class Settings(BaseSettings):
|
|
|
359
240
|
|
|
360
241
|
@classmethod
|
|
361
242
|
def from_toml(cls, toml_path: Path | None = None, **kwargs: Any) -> "Settings":
|
|
362
|
-
"""Create Settings instance from TOML configuration.
|
|
363
|
-
|
|
364
|
-
Args:
|
|
365
|
-
toml_path: Path to TOML configuration file. If None, auto-discovers file.
|
|
366
|
-
**kwargs: Additional keyword arguments to override config values
|
|
367
|
-
|
|
368
|
-
Returns:
|
|
369
|
-
Settings: Configured Settings instance
|
|
370
|
-
"""
|
|
371
|
-
# Use the more generic from_config method
|
|
243
|
+
"""Create Settings instance from TOML configuration."""
|
|
372
244
|
return cls.from_config(config_path=toml_path, **kwargs)
|
|
373
245
|
|
|
246
|
+
# ------------------------------
|
|
247
|
+
# Internal helpers (merging/overrides)
|
|
248
|
+
# ------------------------------
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _env_has_prefix(prefix: str) -> bool:
|
|
251
|
+
p = prefix.upper()
|
|
252
|
+
return any(k.upper().startswith(p) for k in os.environ)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _merge_model(
|
|
256
|
+
model: BaseModel, overrides: dict[str, Any], env_prefix: str
|
|
257
|
+
) -> BaseModel:
|
|
258
|
+
"""
|
|
259
|
+
Deep-merge a dict of overrides into a BaseModel while preserving env-var precedence.
|
|
260
|
+
env_prefix should end with '__' when called for nested fields.
|
|
261
|
+
"""
|
|
262
|
+
update_payload: dict[str, Any] = {}
|
|
263
|
+
|
|
264
|
+
for field_name, override_value in overrides.items():
|
|
265
|
+
field_env_key = f"{env_prefix}{field_name.upper()}"
|
|
266
|
+
# If an env var exists for this field, do NOT override from file.
|
|
267
|
+
if os.getenv(field_env_key) is not None:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
current_value = getattr(model, field_name, None)
|
|
271
|
+
|
|
272
|
+
if isinstance(current_value, BaseModel) and isinstance(
|
|
273
|
+
override_value, dict
|
|
274
|
+
):
|
|
275
|
+
nested_prefix = f"{field_env_key}__"
|
|
276
|
+
merged_nested = Settings._merge_model(
|
|
277
|
+
current_value, override_value, nested_prefix
|
|
278
|
+
)
|
|
279
|
+
update_payload[field_name] = merged_nested
|
|
280
|
+
elif isinstance(current_value, dict) and isinstance(override_value, dict):
|
|
281
|
+
# Deep-merge dict but skip keys that have env overrides
|
|
282
|
+
merged_dict = current_value.copy()
|
|
283
|
+
for nk, nv in override_value.items():
|
|
284
|
+
nested_env_key = f"{field_env_key}__{nk.upper()}"
|
|
285
|
+
if os.getenv(nested_env_key) is None:
|
|
286
|
+
if isinstance(merged_dict.get(nk), dict) and isinstance(
|
|
287
|
+
nv, dict
|
|
288
|
+
):
|
|
289
|
+
# deep merge nested dicts with respect to env
|
|
290
|
+
merged_dict[nk] = Settings._merge_dict(
|
|
291
|
+
merged_dict.get(nk, {}), nv, f"{nested_env_key}__"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
merged_dict[nk] = nv
|
|
295
|
+
update_payload[field_name] = merged_dict
|
|
296
|
+
else:
|
|
297
|
+
update_payload[field_name] = override_value
|
|
298
|
+
|
|
299
|
+
if not update_payload:
|
|
300
|
+
return model
|
|
301
|
+
return model.model_copy(update=update_payload)
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _merge_dict(
|
|
305
|
+
base: dict[str, Any], overrides: dict[str, Any], env_prefix: str
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
"""
|
|
308
|
+
Deep-merge dicts while respecting env-var precedence using the given env_prefix (no trailing __ required).
|
|
309
|
+
"""
|
|
310
|
+
out = dict(base)
|
|
311
|
+
for k, v in overrides.items():
|
|
312
|
+
key_env = f"{env_prefix}{k.upper()}"
|
|
313
|
+
if os.getenv(key_env) is not None:
|
|
314
|
+
continue
|
|
315
|
+
if isinstance(out.get(k), dict) and isinstance(v, dict):
|
|
316
|
+
out[k] = Settings._merge_dict(out[k], v, f"{key_env}__")
|
|
317
|
+
else:
|
|
318
|
+
out[k] = v
|
|
319
|
+
return out
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _merge_plugins(
|
|
323
|
+
current_plugins: dict[str, Any], overrides: dict[str, Any]
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
"""
|
|
326
|
+
Merge plugin configuration trees with env precedence at both plugin and nested key levels.
|
|
327
|
+
"""
|
|
328
|
+
merged = dict(current_plugins)
|
|
329
|
+
for plugin_name, plugin_cfg in overrides.items():
|
|
330
|
+
env_prefix = f"PLUGINS__{plugin_name.upper()}__"
|
|
331
|
+
|
|
332
|
+
# If any env for this plugin exists, we keep current_plugins[plugin_name] as-is,
|
|
333
|
+
# but we still allow env-free nested keys to merge if we already have a dict.
|
|
334
|
+
if isinstance(plugin_cfg, dict):
|
|
335
|
+
if Settings._env_has_prefix(env_prefix):
|
|
336
|
+
# Partial merge respecting env at nested levels if the plugin already exists as a dict.
|
|
337
|
+
if isinstance(merged.get(plugin_name), dict):
|
|
338
|
+
merged[plugin_name] = Settings._merge_dict(
|
|
339
|
+
merged[plugin_name],
|
|
340
|
+
plugin_cfg,
|
|
341
|
+
env_prefix,
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
# Keep existing unless it's missing entirely.
|
|
345
|
+
merged.setdefault(plugin_name, merged.get(plugin_name, {}))
|
|
346
|
+
else:
|
|
347
|
+
existing = merged.get(plugin_name, {})
|
|
348
|
+
if isinstance(existing, dict):
|
|
349
|
+
merged[plugin_name] = Settings._merge_dict(
|
|
350
|
+
existing, plugin_cfg, env_prefix
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
merged[plugin_name] = plugin_cfg
|
|
354
|
+
else:
|
|
355
|
+
# Non-dict plugin setting: only apply if no top-level env overrides present.
|
|
356
|
+
if not Settings._env_has_prefix(env_prefix):
|
|
357
|
+
merged[plugin_name] = plugin_cfg
|
|
358
|
+
return merged
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _apply_overrides(target: Any, overrides: dict[str, Any]) -> None:
|
|
362
|
+
"""
|
|
363
|
+
Apply CLI/kwargs overrides after file and env processing.
|
|
364
|
+
Dicts are shallow-merged; nested BaseModels recurse.
|
|
365
|
+
"""
|
|
366
|
+
for k, v in overrides.items():
|
|
367
|
+
if (
|
|
368
|
+
isinstance(v, dict)
|
|
369
|
+
and hasattr(target, k)
|
|
370
|
+
and isinstance(getattr(target, k), (BaseModel | dict))
|
|
371
|
+
):
|
|
372
|
+
sub = getattr(target, k)
|
|
373
|
+
if isinstance(sub, BaseModel):
|
|
374
|
+
# Apply directly field-by-field
|
|
375
|
+
Settings._apply_overrides(sub, v)
|
|
376
|
+
elif isinstance(sub, dict):
|
|
377
|
+
sub.update(v)
|
|
378
|
+
else:
|
|
379
|
+
setattr(target, k, v)
|
|
380
|
+
|
|
381
|
+
# ------------------------------
|
|
382
|
+
# Factory
|
|
383
|
+
# ------------------------------
|
|
374
384
|
@classmethod
|
|
375
385
|
def from_config(
|
|
376
|
-
cls,
|
|
386
|
+
cls,
|
|
387
|
+
config_path: Path | str | None = None,
|
|
388
|
+
cli_context: dict[str, Any] | None = None,
|
|
389
|
+
**kwargs: Any,
|
|
377
390
|
) -> "Settings":
|
|
378
|
-
"""Create Settings instance from configuration file.
|
|
391
|
+
"""Create Settings instance from configuration file with env precedence and safe merging."""
|
|
392
|
+
logger = get_logger(__name__)
|
|
379
393
|
|
|
380
|
-
|
|
381
|
-
config_path: Path to configuration file. Can be:
|
|
382
|
-
- None: Auto-discover config file or use CONFIG_FILE env var
|
|
383
|
-
- Path or str: Use this specific config file
|
|
384
|
-
**kwargs: Additional keyword arguments to override config values
|
|
394
|
+
global _CONFIG_MISSING_LOGGED
|
|
385
395
|
|
|
386
|
-
Returns:
|
|
387
|
-
Settings: Configured Settings instance
|
|
388
|
-
"""
|
|
389
|
-
# Check for CONFIG_FILE environment variable first
|
|
390
396
|
if config_path is None:
|
|
391
397
|
config_path_env = os.environ.get("CONFIG_FILE")
|
|
392
398
|
if config_path_env:
|
|
393
399
|
config_path = Path(config_path_env)
|
|
394
400
|
|
|
395
|
-
# Convert string to Path if needed
|
|
396
401
|
if isinstance(config_path, str):
|
|
397
402
|
config_path = Path(config_path)
|
|
398
403
|
|
|
399
|
-
# Auto-discover config file if not provided
|
|
400
404
|
if config_path is None:
|
|
401
405
|
config_path = find_toml_config_file()
|
|
402
406
|
|
|
403
|
-
|
|
404
|
-
config_data = {}
|
|
407
|
+
config_data: dict[str, Any] = {}
|
|
405
408
|
if config_path and config_path.exists():
|
|
406
409
|
config_data = cls.load_config_file(config_path)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
def __init__(self) -> None:
|
|
419
|
-
self._settings: Settings | None = None
|
|
420
|
-
self._config_path: Path | None = None
|
|
421
|
-
self._logging_configured = False
|
|
422
|
-
|
|
423
|
-
def load_settings(
|
|
424
|
-
self,
|
|
425
|
-
config_path: Path | None = None,
|
|
426
|
-
cli_overrides: dict[str, Any] | None = None,
|
|
427
|
-
) -> Settings:
|
|
428
|
-
"""Load settings with CLI overrides and caching."""
|
|
429
|
-
if self._settings is None or config_path != self._config_path:
|
|
430
|
-
try:
|
|
431
|
-
self._settings = Settings.from_config(
|
|
432
|
-
config_path=config_path, **(cli_overrides or {})
|
|
433
|
-
)
|
|
434
|
-
self._config_path = config_path
|
|
435
|
-
except Exception as e:
|
|
436
|
-
raise ConfigurationError(f"Failed to load configuration: {e}") from e
|
|
437
|
-
|
|
438
|
-
return self._settings
|
|
439
|
-
|
|
440
|
-
def setup_logging(self, log_level: str | None = None) -> None:
|
|
441
|
-
"""Configure logging once based on settings."""
|
|
442
|
-
if self._logging_configured:
|
|
443
|
-
return
|
|
444
|
-
|
|
445
|
-
# Import here to avoid circular import
|
|
446
|
-
|
|
447
|
-
effective_level = log_level or (
|
|
448
|
-
self._settings.server.log_level if self._settings else "INFO"
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
# Determine format based on log level - Rich for DEBUG, JSON for production
|
|
452
|
-
format_type = "rich" if effective_level.upper() == "DEBUG" else "json"
|
|
453
|
-
|
|
454
|
-
# setup_dual_logging(
|
|
455
|
-
# level=effective_level,
|
|
456
|
-
# format_type=format_type,
|
|
457
|
-
# configure_uvicorn=True,
|
|
458
|
-
# verbose_tracebacks=effective_level.upper() == "DEBUG",
|
|
459
|
-
# )
|
|
460
|
-
self._logging_configured = True
|
|
461
|
-
|
|
462
|
-
def get_cli_overrides_from_args(self, **cli_args: Any) -> dict[str, Any]:
|
|
463
|
-
"""Extract non-None CLI arguments as configuration overrides."""
|
|
464
|
-
overrides = {}
|
|
465
|
-
|
|
466
|
-
# Server settings
|
|
467
|
-
server_settings = {}
|
|
468
|
-
for key in ["host", "port", "reload", "log_level", "log_file"]:
|
|
469
|
-
if cli_args.get(key) is not None:
|
|
470
|
-
server_settings[key] = cli_args[key]
|
|
471
|
-
if server_settings:
|
|
472
|
-
overrides["server"] = server_settings
|
|
473
|
-
|
|
474
|
-
# Security settings
|
|
475
|
-
if cli_args.get("auth_token") is not None:
|
|
476
|
-
overrides["security"] = {"auth_token": cli_args["auth_token"]}
|
|
477
|
-
|
|
478
|
-
# Claude settings
|
|
479
|
-
claude_settings = {}
|
|
480
|
-
if cli_args.get("claude_cli_path") is not None:
|
|
481
|
-
claude_settings["cli_path"] = cli_args["claude_cli_path"]
|
|
482
|
-
|
|
483
|
-
# Direct Claude settings (not nested in code_options)
|
|
484
|
-
for key in [
|
|
485
|
-
"sdk_message_mode",
|
|
486
|
-
"system_prompt_injection_mode",
|
|
487
|
-
"builtin_permissions",
|
|
488
|
-
]:
|
|
489
|
-
if cli_args.get(key) is not None:
|
|
490
|
-
claude_settings[key] = cli_args[key]
|
|
491
|
-
|
|
492
|
-
# Handle pool configuration
|
|
493
|
-
if cli_args.get("sdk_pool") is not None:
|
|
494
|
-
claude_settings["sdk_pool"] = {"enabled": cli_args["sdk_pool"]}
|
|
495
|
-
|
|
496
|
-
if cli_args.get("sdk_pool_size") is not None:
|
|
497
|
-
if "sdk_pool" not in claude_settings:
|
|
498
|
-
claude_settings["sdk_pool"] = {}
|
|
499
|
-
claude_settings["sdk_pool"]["pool_size"] = cli_args["sdk_pool_size"]
|
|
500
|
-
|
|
501
|
-
if cli_args.get("sdk_session_pool") is not None:
|
|
502
|
-
claude_settings["sdk_session_pool"] = {
|
|
503
|
-
"enabled": cli_args["sdk_session_pool"]
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
# Claude Code options
|
|
507
|
-
claude_opts = {}
|
|
508
|
-
for key in [
|
|
509
|
-
"max_thinking_tokens",
|
|
510
|
-
"permission_mode",
|
|
511
|
-
"cwd",
|
|
512
|
-
"max_turns",
|
|
513
|
-
"append_system_prompt",
|
|
514
|
-
"permission_prompt_tool_name",
|
|
515
|
-
"continue_conversation",
|
|
516
|
-
]:
|
|
517
|
-
if cli_args.get(key) is not None:
|
|
518
|
-
claude_opts[key] = cli_args[key]
|
|
519
|
-
|
|
520
|
-
# Handle comma-separated lists
|
|
521
|
-
for key in ["allowed_tools", "disallowed_tools"]:
|
|
522
|
-
if cli_args.get(key):
|
|
523
|
-
claude_opts[key] = [tool.strip() for tool in cli_args[key].split(",")]
|
|
524
|
-
|
|
525
|
-
if claude_opts:
|
|
526
|
-
claude_settings["code_options"] = claude_opts
|
|
527
|
-
|
|
528
|
-
if claude_settings:
|
|
529
|
-
overrides["claude"] = claude_settings
|
|
530
|
-
|
|
531
|
-
# CORS settings
|
|
532
|
-
if cli_args.get("cors_origins"):
|
|
533
|
-
overrides["cors"] = {
|
|
534
|
-
"origins": [
|
|
535
|
-
origin.strip() for origin in cli_args["cors_origins"].split(",")
|
|
536
|
-
]
|
|
410
|
+
logger.debug(
|
|
411
|
+
"config_file_loaded",
|
|
412
|
+
path=str(config_path),
|
|
413
|
+
category="config",
|
|
414
|
+
)
|
|
415
|
+
elif not _CONFIG_MISSING_LOGGED:
|
|
416
|
+
suggestion = f"ccproxy config init --output-dir {get_ccproxy_config_dir()}"
|
|
417
|
+
log_kwargs: dict[str, Any] = {
|
|
418
|
+
"category": "config",
|
|
419
|
+
"suggested_command": suggestion,
|
|
537
420
|
}
|
|
421
|
+
if config_path is not None:
|
|
422
|
+
log_kwargs["path"] = str(config_path)
|
|
423
|
+
logger.warning("config_file_missing", **log_kwargs)
|
|
424
|
+
_CONFIG_MISSING_LOGGED = True
|
|
425
|
+
|
|
426
|
+
cls._validate_deprecated_keys(config_data)
|
|
427
|
+
|
|
428
|
+
# Start from env + .env via BaseSettings
|
|
429
|
+
settings = cls()
|
|
430
|
+
|
|
431
|
+
# Merge file-based configuration with env-var precedence
|
|
432
|
+
for key, value in config_data.items():
|
|
433
|
+
if not hasattr(settings, key):
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
if key == "plugins" and isinstance(value, dict):
|
|
437
|
+
current_plugins = getattr(settings, key, {})
|
|
438
|
+
merged_plugins = cls._merge_plugins(current_plugins, value)
|
|
439
|
+
setattr(settings, key, merged_plugins)
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
current_attr = getattr(settings, key)
|
|
443
|
+
|
|
444
|
+
if isinstance(value, dict) and isinstance(current_attr, BaseModel):
|
|
445
|
+
merged_model = cls._merge_model(current_attr, value, f"{key.upper()}__")
|
|
446
|
+
setattr(settings, key, merged_model)
|
|
447
|
+
else:
|
|
448
|
+
# Only set top-level simple types if there is no top-level env override
|
|
449
|
+
env_key = key.upper()
|
|
450
|
+
if os.getenv(env_key) is None:
|
|
451
|
+
setattr(settings, key, value)
|
|
452
|
+
|
|
453
|
+
# Smart default: if no config file exists and enabled_plugins is still None,
|
|
454
|
+
# set a curated default list of core plugins
|
|
455
|
+
if not config_path or not config_path.exists():
|
|
456
|
+
if settings.enabled_plugins is None:
|
|
457
|
+
settings.enabled_plugins = DEFAULT_ENABLED_PLUGINS
|
|
458
|
+
|
|
459
|
+
# Apply direct kwargs overrides (highest precedence within process)
|
|
460
|
+
if kwargs:
|
|
461
|
+
cls._apply_overrides(settings, kwargs)
|
|
462
|
+
|
|
463
|
+
# Apply CLI context (explicit flags)
|
|
464
|
+
if cli_context:
|
|
465
|
+
# Store raw CLI context for plugin access
|
|
466
|
+
settings.cli_context = cli_context
|
|
467
|
+
|
|
468
|
+
# Apply common serve CLI overrides directly to settings
|
|
469
|
+
server_overrides: dict[str, Any] = {}
|
|
470
|
+
if cli_context.get("host") is not None:
|
|
471
|
+
server_overrides["host"] = cli_context["host"]
|
|
472
|
+
if cli_context.get("port") is not None:
|
|
473
|
+
server_overrides["port"] = cli_context["port"]
|
|
474
|
+
if cli_context.get("reload") is not None:
|
|
475
|
+
server_overrides["reload"] = cli_context["reload"]
|
|
476
|
+
|
|
477
|
+
logging_overrides: dict[str, Any] = {}
|
|
478
|
+
if cli_context.get("log_level") is not None:
|
|
479
|
+
logging_overrides["level"] = cli_context["log_level"]
|
|
480
|
+
if cli_context.get("log_file") is not None:
|
|
481
|
+
logging_overrides["file"] = cli_context["log_file"]
|
|
482
|
+
|
|
483
|
+
security_overrides: dict[str, Any] = {}
|
|
484
|
+
if cli_context.get("auth_token") is not None:
|
|
485
|
+
security_overrides["auth_token"] = cli_context["auth_token"]
|
|
486
|
+
|
|
487
|
+
if server_overrides:
|
|
488
|
+
cls._apply_overrides(settings, {"server": server_overrides})
|
|
489
|
+
if logging_overrides:
|
|
490
|
+
cls._apply_overrides(settings, {"logging": logging_overrides})
|
|
491
|
+
if security_overrides:
|
|
492
|
+
cls._apply_overrides(settings, {"security": security_overrides})
|
|
493
|
+
|
|
494
|
+
# Apply plugin enable/disable lists if provided
|
|
495
|
+
enabled_plugins = cli_context.get("enabled_plugins")
|
|
496
|
+
disabled_plugins = cli_context.get("disabled_plugins")
|
|
497
|
+
if enabled_plugins is not None:
|
|
498
|
+
settings.enabled_plugins = list(enabled_plugins)
|
|
499
|
+
if disabled_plugins is not None:
|
|
500
|
+
settings.disabled_plugins = list(disabled_plugins)
|
|
538
501
|
|
|
539
|
-
return
|
|
540
|
-
|
|
541
|
-
def reset(self) -> None:
|
|
542
|
-
"""Reset configuration state (useful for testing)."""
|
|
543
|
-
self._settings = None
|
|
544
|
-
self._config_path = None
|
|
545
|
-
self._logging_configured = False
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
# Global configuration manager instance
|
|
549
|
-
config_manager = ConfigurationManager()
|
|
550
|
-
|
|
551
|
-
logger = structlog.get_logger(__name__)
|
|
502
|
+
return settings
|
|
552
503
|
|
|
504
|
+
def get_cli_context(self) -> dict[str, Any]:
|
|
505
|
+
"""Get CLI context for plugin access."""
|
|
506
|
+
return self.cli_context
|
|
553
507
|
|
|
554
|
-
|
|
555
|
-
|
|
508
|
+
class LLMSettings(BaseModel):
|
|
509
|
+
"""LLM-specific feature toggles and defaults."""
|
|
556
510
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
511
|
+
openai_thinking_xml: bool = Field(
|
|
512
|
+
default=True, description="Serialize thinking as XML in OpenAI streams"
|
|
513
|
+
)
|
|
560
514
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# Check for CLI overrides from environment variable
|
|
566
|
-
cli_overrides = {}
|
|
567
|
-
cli_overrides_json = os.environ.get("CCPROXY_CONFIG_OVERRIDES")
|
|
568
|
-
if cli_overrides_json:
|
|
569
|
-
with contextlib.suppress(json.JSONDecodeError):
|
|
570
|
-
cli_overrides = json.loads(cli_overrides_json)
|
|
571
|
-
|
|
572
|
-
settings = Settings.from_config(config_path=config_path, **cli_overrides)
|
|
573
|
-
return settings
|
|
574
|
-
except Exception as e:
|
|
575
|
-
# If settings can't be loaded (e.g., missing API key),
|
|
576
|
-
# this will be handled by the caller
|
|
577
|
-
raise ValueError(f"Configuration error: {e}") from e
|
|
515
|
+
llm: LLMSettings = Field(
|
|
516
|
+
default_factory=LLMSettings,
|
|
517
|
+
description="Large Language Model (LLM) settings",
|
|
518
|
+
)
|