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
|
@@ -5,14 +5,25 @@ import secrets
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
8
9
|
import typer
|
|
9
10
|
from click import get_current_context
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
from pydantic.fields import FieldInfo
|
|
12
13
|
|
|
13
|
-
from ccproxy._version import __version__
|
|
14
14
|
from ccproxy.cli.helpers import get_rich_toolkit
|
|
15
|
-
from ccproxy.config.settings import Settings
|
|
15
|
+
from ccproxy.config.settings import Settings
|
|
16
|
+
from ccproxy.core._version import __version__
|
|
17
|
+
from ccproxy.services.container import ServiceContainer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_service_container() -> ServiceContainer:
|
|
24
|
+
"""Create a service container for the config commands."""
|
|
25
|
+
settings = Settings.from_config(config_path=get_config_path_from_context())
|
|
26
|
+
return ServiceContainer(settings)
|
|
16
27
|
|
|
17
28
|
|
|
18
29
|
def _create_config_table(title: str, rows: list[tuple[str, str, str]]) -> Any:
|
|
@@ -20,9 +31,9 @@ def _create_config_table(title: str, rows: list[tuple[str, str, str]]) -> Any:
|
|
|
20
31
|
from rich.table import Table
|
|
21
32
|
|
|
22
33
|
table = Table(title=title, show_header=True, header_style="bold magenta")
|
|
23
|
-
table.add_column("Setting", style="cyan",
|
|
24
|
-
table.add_column("Value", style="green")
|
|
25
|
-
table.add_column("Description", style="dim")
|
|
34
|
+
table.add_column("Setting", style="cyan", overflow="fold")
|
|
35
|
+
table.add_column("Value", style="green", overflow="fold")
|
|
36
|
+
table.add_column("Description", style="dim", overflow="fold")
|
|
26
37
|
|
|
27
38
|
for setting, value, description in rows:
|
|
28
39
|
table.add_row(setting, value, description)
|
|
@@ -39,7 +50,6 @@ def _format_value(value: Any) -> str:
|
|
|
39
50
|
elif isinstance(value, str):
|
|
40
51
|
if not value:
|
|
41
52
|
return "[dim]Not set[/dim]"
|
|
42
|
-
# Special handling for sensitive values
|
|
43
53
|
if any(
|
|
44
54
|
keyword in value.lower()
|
|
45
55
|
for keyword in ["token", "key", "secret", "password"]
|
|
@@ -64,7 +74,6 @@ def _get_field_description(field_info: FieldInfo) -> str:
|
|
|
64
74
|
"""Get a human-readable description from a Pydantic field."""
|
|
65
75
|
if field_info.description:
|
|
66
76
|
return field_info.description
|
|
67
|
-
# Generate a basic description from the field name
|
|
68
77
|
return "Configuration setting"
|
|
69
78
|
|
|
70
79
|
|
|
@@ -74,14 +83,13 @@ def _generate_config_rows_from_model(
|
|
|
74
83
|
"""Generate configuration rows from a Pydantic model dynamically."""
|
|
75
84
|
rows = []
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
field_definitions = model.__class__.model_fields
|
|
87
|
+
|
|
88
|
+
for field_name, _field_info in field_definitions.items():
|
|
78
89
|
field_value = getattr(model, field_name)
|
|
79
90
|
display_name = f"{prefix}{field_name}" if prefix else field_name
|
|
80
91
|
|
|
81
|
-
# If the field value is also a BaseModel, we might want to flatten it
|
|
82
92
|
if isinstance(field_value, BaseModel):
|
|
83
|
-
# For nested models, we can either flatten or show as a summary
|
|
84
|
-
# For now, let's show a summary and then add sub-rows
|
|
85
93
|
model_name = field_value.__class__.__name__
|
|
86
94
|
rows.append(
|
|
87
95
|
(
|
|
@@ -91,11 +99,9 @@ def _generate_config_rows_from_model(
|
|
|
91
99
|
)
|
|
92
100
|
)
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
sub_rows = _generate_config_rows_from_model(field_value, f"{display_name}_")
|
|
102
|
+
sub_rows = _generate_config_rows_from_model(field_value, f"{display_name}.")
|
|
96
103
|
rows.extend(sub_rows)
|
|
97
104
|
else:
|
|
98
|
-
# Regular field
|
|
99
105
|
formatted_value = _format_value(field_value)
|
|
100
106
|
description = _get_field_description(_field_info)
|
|
101
107
|
rows.append((display_name, formatted_value, description))
|
|
@@ -109,41 +115,68 @@ def _group_config_rows(
|
|
|
109
115
|
"""Group configuration rows by their top-level section."""
|
|
110
116
|
groups: dict[str, list[tuple[str, str, str]]] = {}
|
|
111
117
|
|
|
118
|
+
CATEGORY_PREFIXES = {
|
|
119
|
+
"server_": "Server Configuration",
|
|
120
|
+
"security_": "Security Configuration",
|
|
121
|
+
"cors_": "CORS Configuration",
|
|
122
|
+
"claude_": "Claude CLI Configuration",
|
|
123
|
+
"auth_": "Authentication Configuration",
|
|
124
|
+
"docker_": "Docker Configuration",
|
|
125
|
+
"observability_": "Observability Configuration",
|
|
126
|
+
"scheduler_": "Scheduler Configuration",
|
|
127
|
+
"pricing_": "Pricing Configuration",
|
|
128
|
+
}
|
|
129
|
+
|
|
112
130
|
for setting, value, description in rows:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
group_name = "Claude CLI Configuration"
|
|
122
|
-
elif setting.startswith("reverse_proxy"):
|
|
123
|
-
group_name = "Reverse Proxy Configuration"
|
|
124
|
-
elif setting.startswith("auth"):
|
|
125
|
-
group_name = "Authentication Configuration"
|
|
126
|
-
elif setting.startswith("docker"):
|
|
127
|
-
group_name = "Docker Configuration"
|
|
128
|
-
elif setting.startswith("observability"):
|
|
129
|
-
group_name = "Observability Configuration"
|
|
130
|
-
elif setting.startswith("scheduler"):
|
|
131
|
-
group_name = "Scheduler Configuration"
|
|
132
|
-
elif setting.startswith("pricing"):
|
|
133
|
-
group_name = "Pricing Configuration"
|
|
131
|
+
normalized_setting = setting
|
|
132
|
+
group_name = "General Configuration"
|
|
133
|
+
|
|
134
|
+
for prefix, group in CATEGORY_PREFIXES.items():
|
|
135
|
+
if setting.startswith(prefix):
|
|
136
|
+
normalized_setting = setting[len(prefix) :]
|
|
137
|
+
group_name = group
|
|
138
|
+
break
|
|
134
139
|
else:
|
|
135
|
-
|
|
140
|
+
if setting.startswith("server"):
|
|
141
|
+
group_name = "Server Configuration"
|
|
142
|
+
elif setting.startswith("security"):
|
|
143
|
+
group_name = "Security Configuration"
|
|
144
|
+
elif setting.startswith("cors"):
|
|
145
|
+
group_name = "CORS Configuration"
|
|
146
|
+
elif setting.startswith("claude"):
|
|
147
|
+
group_name = "Claude CLI Configuration"
|
|
148
|
+
elif setting.startswith("auth"):
|
|
149
|
+
group_name = "Authentication Configuration"
|
|
150
|
+
elif setting.startswith("docker"):
|
|
151
|
+
group_name = "Docker Configuration"
|
|
152
|
+
elif setting.startswith("observability"):
|
|
153
|
+
group_name = "Observability Configuration"
|
|
154
|
+
elif setting.startswith("scheduler"):
|
|
155
|
+
group_name = "Scheduler Configuration"
|
|
156
|
+
elif setting.startswith("pricing"):
|
|
157
|
+
group_name = "Pricing Configuration"
|
|
158
|
+
|
|
159
|
+
if "." in normalized_setting:
|
|
160
|
+
normalized_setting = normalized_setting.split(".")[-1]
|
|
136
161
|
|
|
137
162
|
if group_name not in groups:
|
|
138
163
|
groups[group_name] = []
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
clean_setting = setting.split("_", 1)[1] if "_" in setting else setting
|
|
142
|
-
groups[group_name].append((clean_setting, value, description))
|
|
165
|
+
groups[group_name].append((normalized_setting, value, description))
|
|
143
166
|
|
|
144
167
|
return groups
|
|
145
168
|
|
|
146
169
|
|
|
170
|
+
def _is_hidden_in_example(field_info: FieldInfo) -> bool:
|
|
171
|
+
"""Determine if a field should be omitted from generated example configs."""
|
|
172
|
+
|
|
173
|
+
if bool(field_info.exclude):
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
extra = getattr(field_info, "json_schema_extra", None) or {}
|
|
177
|
+
return bool(extra.get("config_example_hidden"))
|
|
178
|
+
|
|
179
|
+
|
|
147
180
|
def get_config_path_from_context() -> Path | None:
|
|
148
181
|
"""Get config path from typer context if available."""
|
|
149
182
|
try:
|
|
@@ -152,7 +185,6 @@ def get_config_path_from_context() -> Path | None:
|
|
|
152
185
|
config_path = ctx.obj["config_path"]
|
|
153
186
|
return config_path if config_path is None else Path(config_path)
|
|
154
187
|
except RuntimeError:
|
|
155
|
-
# No active click context (e.g., in tests)
|
|
156
188
|
pass
|
|
157
189
|
return None
|
|
158
190
|
|
|
@@ -169,10 +201,13 @@ app = typer.Typer(
|
|
|
169
201
|
@app.command(name="list")
|
|
170
202
|
def config_list() -> None:
|
|
171
203
|
"""Show current configuration."""
|
|
204
|
+
from ccproxy.cli._settings_help import print_settings_help
|
|
205
|
+
|
|
172
206
|
toolkit = get_rich_toolkit()
|
|
173
207
|
|
|
174
208
|
try:
|
|
175
|
-
|
|
209
|
+
container = _get_service_container()
|
|
210
|
+
settings = container.get_service(Settings)
|
|
176
211
|
|
|
177
212
|
from rich.console import Console
|
|
178
213
|
from rich.panel import Panel
|
|
@@ -180,34 +215,18 @@ def config_list() -> None:
|
|
|
180
215
|
|
|
181
216
|
console = Console()
|
|
182
217
|
|
|
183
|
-
#
|
|
184
|
-
all_rows = _generate_config_rows_from_model(settings)
|
|
185
|
-
|
|
186
|
-
# Add computed fields that aren't part of the model but are useful to display
|
|
187
|
-
all_rows.append(
|
|
188
|
-
("server_url", settings.server_url, "Complete server URL (computed)")
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
# Group rows by configuration section
|
|
192
|
-
grouped_rows = _group_config_rows(all_rows)
|
|
193
|
-
|
|
194
|
-
# Display header
|
|
218
|
+
# Display header panel
|
|
195
219
|
console.print(
|
|
196
220
|
Panel.fit(
|
|
197
221
|
f"[bold]CCProxy API Configuration[/bold]\n[dim]Version: {__version__}[/dim]",
|
|
198
222
|
border_style="blue",
|
|
199
223
|
)
|
|
200
224
|
)
|
|
201
|
-
console.print()
|
|
202
225
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
if section_rows: # Only show sections that have data
|
|
206
|
-
table = _create_config_table(section_name, section_rows)
|
|
207
|
-
console.print(table)
|
|
208
|
-
console.print()
|
|
226
|
+
# Use generic settings display
|
|
227
|
+
print_settings_help(Settings, settings)
|
|
209
228
|
|
|
210
|
-
#
|
|
229
|
+
# Display footer panel
|
|
211
230
|
info_text = Text()
|
|
212
231
|
info_text.append("Configuration loaded from: ", style="bold")
|
|
213
232
|
info_text.append(
|
|
@@ -218,7 +237,20 @@ def config_list() -> None:
|
|
|
218
237
|
Panel(info_text, title="Configuration Sources", border_style="green")
|
|
219
238
|
)
|
|
220
239
|
|
|
240
|
+
except (OSError, PermissionError) as e:
|
|
241
|
+
logger.error("config_list_file_access_error", error=str(e), exc_info=e)
|
|
242
|
+
toolkit.print(f"Error accessing configuration files: {e}", tag="error")
|
|
243
|
+
raise typer.Exit(1) from e
|
|
244
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
245
|
+
logger.error("config_list_parsing_error", error=str(e), exc_info=e)
|
|
246
|
+
toolkit.print(f"Configuration parsing error: {e}", tag="error")
|
|
247
|
+
raise typer.Exit(1) from e
|
|
248
|
+
except ImportError as e:
|
|
249
|
+
logger.error("config_list_import_error", error=str(e), exc_info=e)
|
|
250
|
+
toolkit.print(f"Module import error: {e}", tag="error")
|
|
251
|
+
raise typer.Exit(1) from e
|
|
221
252
|
except Exception as e:
|
|
253
|
+
logger.error("config_list_unexpected_error", error=str(e), exc_info=e)
|
|
222
254
|
toolkit.print(f"Error loading configuration: {e}", tag="error")
|
|
223
255
|
raise typer.Exit(1) from e
|
|
224
256
|
|
|
@@ -243,16 +275,7 @@ def config_init(
|
|
|
243
275
|
help="Overwrite existing configuration files",
|
|
244
276
|
),
|
|
245
277
|
) -> None:
|
|
246
|
-
"""Generate example configuration files.
|
|
247
|
-
|
|
248
|
-
This command creates example configuration files with all available options
|
|
249
|
-
and documentation comments.
|
|
250
|
-
|
|
251
|
-
Examples:
|
|
252
|
-
ccproxy config init # Create TOML config in default location
|
|
253
|
-
ccproxy config init --output-dir ./config # Create in specific directory
|
|
254
|
-
"""
|
|
255
|
-
# Validate format
|
|
278
|
+
"""Generate example configuration files."""
|
|
256
279
|
if format != "toml":
|
|
257
280
|
toolkit = get_rich_toolkit()
|
|
258
281
|
toolkit.print(
|
|
@@ -264,19 +287,15 @@ def config_init(
|
|
|
264
287
|
toolkit = get_rich_toolkit()
|
|
265
288
|
|
|
266
289
|
try:
|
|
267
|
-
from ccproxy.config.
|
|
290
|
+
from ccproxy.config.utils import get_ccproxy_config_dir
|
|
268
291
|
|
|
269
|
-
# Determine output directory
|
|
270
292
|
if output_dir is None:
|
|
271
293
|
output_dir = get_ccproxy_config_dir()
|
|
272
294
|
|
|
273
|
-
# Create output directory if it doesn't exist
|
|
274
295
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
275
296
|
|
|
276
|
-
# Generate configuration dynamically from Settings model
|
|
277
297
|
example_config = _generate_default_config_from_model(Settings)
|
|
278
298
|
|
|
279
|
-
# Determine output file name
|
|
280
299
|
if format == "toml":
|
|
281
300
|
output_file = output_dir / "config.toml"
|
|
282
301
|
if output_file.exists() and not force:
|
|
@@ -286,7 +305,6 @@ def config_init(
|
|
|
286
305
|
)
|
|
287
306
|
raise typer.Exit(1)
|
|
288
307
|
|
|
289
|
-
# Write TOML with comments using dynamic generation
|
|
290
308
|
_write_toml_config_with_comments(output_file, example_config, Settings)
|
|
291
309
|
|
|
292
310
|
toolkit.print(
|
|
@@ -300,7 +318,24 @@ def config_init(
|
|
|
300
318
|
toolkit.print(f" export CONFIG_FILE={output_file}", tag="command")
|
|
301
319
|
toolkit.print(" ccproxy api", tag="command")
|
|
302
320
|
|
|
321
|
+
except (OSError, PermissionError) as e:
|
|
322
|
+
logger.error("config_init_file_access_error", error=str(e), exc_info=e)
|
|
323
|
+
toolkit.print(
|
|
324
|
+
f"Error creating configuration file (permission/IO error): {e}", tag="error"
|
|
325
|
+
)
|
|
326
|
+
raise typer.Exit(1) from e
|
|
327
|
+
except ImportError as e:
|
|
328
|
+
logger.error("config_init_import_error", error=str(e), exc_info=e)
|
|
329
|
+
toolkit.print(f"Module import error: {e}", tag="error")
|
|
330
|
+
raise typer.Exit(1) from e
|
|
331
|
+
except ValueError as e:
|
|
332
|
+
logger.error("config_init_value_error", error=str(e), exc_info=e)
|
|
333
|
+
toolkit.print(f"Configuration value error: {e}", tag="error")
|
|
334
|
+
raise typer.Exit(1) from e
|
|
303
335
|
except Exception as e:
|
|
336
|
+
if isinstance(e, typer.Exit):
|
|
337
|
+
raise
|
|
338
|
+
logger.error("config_init_unexpected_error", error=str(e), exc_info=e)
|
|
304
339
|
toolkit.print(f"Error creating configuration file: {e}", tag="error")
|
|
305
340
|
raise typer.Exit(1) from e
|
|
306
341
|
|
|
@@ -325,23 +360,10 @@ def generate_token(
|
|
|
325
360
|
help="Overwrite existing auth_token without confirmation",
|
|
326
361
|
),
|
|
327
362
|
) -> None:
|
|
328
|
-
"""Generate a secure random token for API authentication.
|
|
329
|
-
|
|
330
|
-
This command generates a secure authentication token that can be used with
|
|
331
|
-
both Anthropic and OpenAI compatible APIs.
|
|
332
|
-
|
|
333
|
-
Use --save to write the token to a TOML configuration file.
|
|
334
|
-
|
|
335
|
-
Examples:
|
|
336
|
-
ccproxy config generate-token # Generate and display token
|
|
337
|
-
ccproxy config generate-token --save # Generate and save to config
|
|
338
|
-
ccproxy config generate-token --save --config-file custom.toml # Save to TOML config
|
|
339
|
-
ccproxy config generate-token --save --force # Overwrite existing token
|
|
340
|
-
"""
|
|
363
|
+
"""Generate a secure random token for API authentication."""
|
|
341
364
|
toolkit = get_rich_toolkit()
|
|
342
365
|
|
|
343
366
|
try:
|
|
344
|
-
# Generate a secure token
|
|
345
367
|
token = secrets.token_urlsafe(32)
|
|
346
368
|
|
|
347
369
|
from rich.console import Console
|
|
@@ -349,7 +371,6 @@ def generate_token(
|
|
|
349
371
|
|
|
350
372
|
console = Console()
|
|
351
373
|
|
|
352
|
-
# Display the generated token
|
|
353
374
|
console.print()
|
|
354
375
|
console.print(
|
|
355
376
|
Panel.fit(
|
|
@@ -359,7 +380,6 @@ def generate_token(
|
|
|
359
380
|
)
|
|
360
381
|
console.print()
|
|
361
382
|
|
|
362
|
-
# Show environment variable commands - server first, then clients
|
|
363
383
|
console.print("[bold]Server Environment Variables:[/bold]")
|
|
364
384
|
console.print(f"[cyan]export SECURITY__AUTH_TOKEN={token}[/cyan]")
|
|
365
385
|
console.print()
|
|
@@ -385,47 +405,40 @@ def generate_token(
|
|
|
385
405
|
|
|
386
406
|
console.print("[bold]Usage with curl (using environment variables):[/bold]")
|
|
387
407
|
console.print("[dim]Anthropic API:[/dim]")
|
|
388
|
-
console.print('[cyan]curl -H "x-api-key: $ANTHROPIC_API_KEY"
|
|
389
|
-
console.print('[cyan] -H "Content-Type: application/json"
|
|
408
|
+
console.print(r'[cyan]curl -H "x-api-key: $ANTHROPIC_API_KEY" \ [/cyan]')
|
|
409
|
+
console.print(r'[cyan] -H "Content-Type: application/json" \ [/cyan]')
|
|
390
410
|
console.print('[cyan] "$ANTHROPIC_BASE_URL/v1/messages"[/cyan]')
|
|
391
411
|
console.print()
|
|
392
412
|
console.print("[dim]OpenAI API:[/dim]")
|
|
393
413
|
console.print(
|
|
394
|
-
'[cyan]curl -H "Authorization: Bearer $OPENAI_API_KEY"
|
|
414
|
+
r'[cyan]curl -H "Authorization: Bearer $OPENAI_API_KEY" \ [/cyan]'
|
|
395
415
|
)
|
|
396
|
-
console.print('[cyan] -H "Content-Type: application/json"
|
|
416
|
+
console.print(r'[cyan] -H "Content-Type: application/json" \ [/cyan]')
|
|
397
417
|
console.print('[cyan] "$OPENAI_BASE_URL/v1/chat/completions"[/cyan]')
|
|
398
418
|
console.print()
|
|
399
419
|
|
|
400
|
-
# Mention the save functionality if not using it
|
|
401
420
|
if not save:
|
|
402
421
|
console.print(
|
|
403
422
|
"[dim]Tip: Use --save to write this token to a configuration file[/dim]"
|
|
404
423
|
)
|
|
405
424
|
console.print()
|
|
406
425
|
|
|
407
|
-
# Save to config file if requested
|
|
408
426
|
if save:
|
|
409
|
-
# Determine config file path
|
|
410
427
|
if config_file is None:
|
|
411
|
-
|
|
412
|
-
from ccproxy.config.discovery import find_toml_config_file
|
|
428
|
+
from ccproxy.config.utils import find_toml_config_file
|
|
413
429
|
|
|
414
430
|
config_file = find_toml_config_file()
|
|
415
431
|
|
|
416
432
|
if config_file is None:
|
|
417
|
-
# Create default config file in current directory
|
|
418
433
|
config_file = Path(".ccproxy.toml")
|
|
419
434
|
|
|
420
435
|
console.print(
|
|
421
436
|
f"[bold]Saving token to configuration file:[/bold] {config_file}"
|
|
422
437
|
)
|
|
423
438
|
|
|
424
|
-
# Detect file format from extension
|
|
425
439
|
file_format = _detect_config_format(config_file)
|
|
426
440
|
console.print(f"[dim]Detected format: {file_format.upper()}[/dim]")
|
|
427
441
|
|
|
428
|
-
# Read existing config or create new one using existing Settings functionality
|
|
429
442
|
config_data = {}
|
|
430
443
|
existing_token = None
|
|
431
444
|
|
|
@@ -436,7 +449,32 @@ def generate_token(
|
|
|
436
449
|
config_data = Settings.load_config_file(config_file)
|
|
437
450
|
existing_token = config_data.get("auth_token")
|
|
438
451
|
console.print("[dim]Found existing configuration file[/dim]")
|
|
452
|
+
except (OSError, PermissionError) as e:
|
|
453
|
+
logger.warning(
|
|
454
|
+
"generate_token_config_file_access_error",
|
|
455
|
+
error=str(e),
|
|
456
|
+
exc_info=e,
|
|
457
|
+
)
|
|
458
|
+
console.print(
|
|
459
|
+
f"[yellow]Warning: Could not access existing config file: {e}[/yellow]"
|
|
460
|
+
)
|
|
461
|
+
console.print("[dim]Will create new configuration file[/dim]")
|
|
462
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
463
|
+
logger.warning(
|
|
464
|
+
"generate_token_config_file_parse_error",
|
|
465
|
+
error=str(e),
|
|
466
|
+
exc_info=e,
|
|
467
|
+
)
|
|
468
|
+
console.print(
|
|
469
|
+
f"[yellow]Warning: Could not parse existing config file: {e}[/yellow]"
|
|
470
|
+
)
|
|
471
|
+
console.print("[dim]Will create new configuration file[/dim]")
|
|
439
472
|
except Exception as e:
|
|
473
|
+
logger.warning(
|
|
474
|
+
"generate_token_config_file_read_error",
|
|
475
|
+
error=str(e),
|
|
476
|
+
exc_info=e,
|
|
477
|
+
)
|
|
440
478
|
console.print(
|
|
441
479
|
f"[yellow]Warning: Could not read existing config file: {e}[/yellow]"
|
|
442
480
|
)
|
|
@@ -444,7 +482,6 @@ def generate_token(
|
|
|
444
482
|
else:
|
|
445
483
|
console.print("[dim]Will create new configuration file[/dim]")
|
|
446
484
|
|
|
447
|
-
# Check for existing token and ask for confirmation if needed
|
|
448
485
|
if existing_token and not force:
|
|
449
486
|
console.print()
|
|
450
487
|
console.print(
|
|
@@ -458,10 +495,8 @@ def generate_token(
|
|
|
458
495
|
console.print("[dim]Token generation cancelled[/dim]")
|
|
459
496
|
return
|
|
460
497
|
|
|
461
|
-
# Update auth_token in config
|
|
462
498
|
config_data["auth_token"] = token
|
|
463
499
|
|
|
464
|
-
# Write updated config in the appropriate format
|
|
465
500
|
_write_config_file(config_file, config_data, file_format)
|
|
466
501
|
|
|
467
502
|
console.print(f"[green]✓[/green] Token saved to {config_file}")
|
|
@@ -473,7 +508,20 @@ def generate_token(
|
|
|
473
508
|
console.print(f"[cyan]export CONFIG_FILE={config_file}[/cyan]")
|
|
474
509
|
console.print("[cyan]ccproxy api[/cyan]")
|
|
475
510
|
|
|
511
|
+
except (OSError, PermissionError) as e:
|
|
512
|
+
logger.error("generate_token_file_write_error", error=str(e), exc_info=e)
|
|
513
|
+
toolkit.print(f"Error writing configuration file: {e}", tag="error")
|
|
514
|
+
raise typer.Exit(1) from e
|
|
515
|
+
except ValueError as e:
|
|
516
|
+
logger.error("generate_token_value_error", error=str(e), exc_info=e)
|
|
517
|
+
toolkit.print(f"Token generation configuration error: {e}", tag="error")
|
|
518
|
+
raise typer.Exit(1) from e
|
|
519
|
+
except ImportError as e:
|
|
520
|
+
logger.error("generate_token_import_error", error=str(e), exc_info=e)
|
|
521
|
+
toolkit.print(f"Module import error: {e}", tag="error")
|
|
522
|
+
raise typer.Exit(1) from e
|
|
476
523
|
except Exception as e:
|
|
524
|
+
logger.error("generate_token_unexpected_error", error=str(e), exc_info=e)
|
|
477
525
|
toolkit.print(f"Error generating token: {e}", tag="error")
|
|
478
526
|
raise typer.Exit(1) from e
|
|
479
527
|
|
|
@@ -484,7 +532,6 @@ def _detect_config_format(config_file: Path) -> str:
|
|
|
484
532
|
if suffix in [".toml"]:
|
|
485
533
|
return "toml"
|
|
486
534
|
else:
|
|
487
|
-
# Only TOML is supported
|
|
488
535
|
return "toml"
|
|
489
536
|
|
|
490
537
|
|
|
@@ -492,22 +539,30 @@ def _generate_default_config_from_model(
|
|
|
492
539
|
settings_class: type[Settings],
|
|
493
540
|
) -> dict[str, Any]:
|
|
494
541
|
"""Generate a default configuration dictionary from the Settings model."""
|
|
495
|
-
|
|
542
|
+
from ccproxy.config.settings import DEFAULT_ENABLED_PLUGINS
|
|
543
|
+
|
|
496
544
|
default_settings = settings_class()
|
|
497
545
|
|
|
498
|
-
config_data = {}
|
|
546
|
+
config_data: dict[str, Any] = {}
|
|
547
|
+
|
|
548
|
+
for field_name, field_info in settings_class.model_fields.items():
|
|
549
|
+
if _is_hidden_in_example(field_info):
|
|
550
|
+
continue
|
|
499
551
|
|
|
500
|
-
# Iterate through all fields and extract their default values
|
|
501
|
-
for field_name, _field_info in settings_class.model_fields.items():
|
|
502
552
|
field_value = getattr(default_settings, field_name)
|
|
503
553
|
|
|
554
|
+
# Special case: enabled_plugins should use DEFAULT_ENABLED_PLUGINS for config init
|
|
555
|
+
if field_name == "enabled_plugins" and field_value is None:
|
|
556
|
+
config_data[field_name] = DEFAULT_ENABLED_PLUGINS
|
|
557
|
+
continue
|
|
558
|
+
|
|
504
559
|
if isinstance(field_value, BaseModel):
|
|
505
|
-
|
|
506
|
-
|
|
560
|
+
nested_config = _generate_nested_config_from_model(field_value)
|
|
561
|
+
if nested_config:
|
|
562
|
+
config_data[field_name] = nested_config
|
|
507
563
|
else:
|
|
508
|
-
# Convert Path objects to strings for JSON serialization
|
|
509
564
|
if isinstance(field_value, Path):
|
|
510
|
-
config_data[field_name] = str(field_value)
|
|
565
|
+
config_data[field_name] = str(field_value)
|
|
511
566
|
else:
|
|
512
567
|
config_data[field_name] = field_value
|
|
513
568
|
|
|
@@ -516,17 +571,21 @@ def _generate_default_config_from_model(
|
|
|
516
571
|
|
|
517
572
|
def _generate_nested_config_from_model(model: BaseModel) -> dict[str, Any]:
|
|
518
573
|
"""Generate configuration for nested models."""
|
|
519
|
-
config_data = {}
|
|
574
|
+
config_data: dict[str, Any] = {}
|
|
575
|
+
|
|
576
|
+
for field_name, field_info in model.model_fields.items():
|
|
577
|
+
if _is_hidden_in_example(field_info):
|
|
578
|
+
continue
|
|
520
579
|
|
|
521
|
-
for field_name, _field_info in model.model_fields.items():
|
|
522
580
|
field_value = getattr(model, field_name)
|
|
523
581
|
|
|
524
582
|
if isinstance(field_value, BaseModel):
|
|
525
|
-
|
|
583
|
+
nested_config = _generate_nested_config_from_model(field_value)
|
|
584
|
+
if nested_config:
|
|
585
|
+
config_data[field_name] = nested_config
|
|
526
586
|
else:
|
|
527
|
-
# Convert Path objects to strings for JSON serialization
|
|
528
587
|
if isinstance(field_value, Path):
|
|
529
|
-
config_data[field_name] = str(field_value)
|
|
588
|
+
config_data[field_name] = str(field_value)
|
|
530
589
|
else:
|
|
531
590
|
config_data[field_name] = field_value
|
|
532
591
|
|
|
@@ -543,19 +602,34 @@ def _write_toml_config_with_comments(
|
|
|
543
602
|
f.write("# Most settings are commented out with their default values\n")
|
|
544
603
|
f.write("# Uncomment and modify as needed\n\n")
|
|
545
604
|
|
|
546
|
-
#
|
|
547
|
-
|
|
605
|
+
# Reorder fields to put enabled_plugins first
|
|
606
|
+
field_items = list(settings_class.model_fields.items())
|
|
607
|
+
priority_fields = ["enabled_plugins", "disabled_plugins"]
|
|
608
|
+
|
|
609
|
+
# Separate priority fields from others
|
|
610
|
+
priority_items = [
|
|
611
|
+
(name, info) for name, info in field_items if name in priority_fields
|
|
612
|
+
]
|
|
613
|
+
other_items = [
|
|
614
|
+
(name, info) for name, info in field_items if name not in priority_fields
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
# Combine with priority fields first
|
|
618
|
+
ordered_items = priority_items + other_items
|
|
619
|
+
|
|
620
|
+
for field_name, field_info in ordered_items:
|
|
621
|
+
if _is_hidden_in_example(field_info):
|
|
622
|
+
continue
|
|
623
|
+
|
|
548
624
|
field_value = config_data.get(field_name)
|
|
549
|
-
description = _get_field_description(
|
|
625
|
+
description = _get_field_description(field_info)
|
|
550
626
|
|
|
551
627
|
f.write(f"# {description}\n")
|
|
552
628
|
|
|
553
629
|
if isinstance(field_value, dict):
|
|
554
|
-
# This is a nested model - write as a TOML section
|
|
555
630
|
f.write(f"# [{field_name}]\n")
|
|
556
631
|
_write_toml_section(f, field_value, prefix="# ", level=0)
|
|
557
632
|
else:
|
|
558
|
-
# Simple field - write as commented line
|
|
559
633
|
formatted_value = _format_config_value_for_toml(field_value)
|
|
560
634
|
f.write(f"# {field_name} = {formatted_value}\n")
|
|
561
635
|
|
|
@@ -568,11 +642,9 @@ def _write_toml_section(
|
|
|
568
642
|
"""Write a TOML section with proper indentation and commenting."""
|
|
569
643
|
for key, value in data.items():
|
|
570
644
|
if isinstance(value, dict):
|
|
571
|
-
# Nested section
|
|
572
645
|
f.write(f"{prefix}[{key}]\n")
|
|
573
646
|
_write_toml_section(f, value, prefix, level + 1)
|
|
574
647
|
else:
|
|
575
|
-
# Simple value
|
|
576
648
|
formatted_value = _format_config_value_for_toml(value)
|
|
577
649
|
f.write(f"{prefix}{key} = {formatted_value}\n")
|
|
578
650
|
|
|
@@ -584,28 +656,30 @@ def _format_config_value_for_toml(value: Any) -> str:
|
|
|
584
656
|
elif isinstance(value, bool):
|
|
585
657
|
return "true" if value else "false"
|
|
586
658
|
elif isinstance(value, str):
|
|
587
|
-
return f'"{value}"'
|
|
659
|
+
return f'"{value}"' # Correctly escape quotes within strings
|
|
588
660
|
elif isinstance(value, int | float):
|
|
589
661
|
return str(value)
|
|
590
662
|
elif isinstance(value, list):
|
|
591
663
|
if not value:
|
|
592
664
|
return "[]"
|
|
593
|
-
# Format list items
|
|
594
665
|
formatted_items = []
|
|
595
666
|
for item in value:
|
|
596
667
|
if isinstance(item, str):
|
|
597
|
-
formatted_items.append(
|
|
668
|
+
formatted_items.append(
|
|
669
|
+
f'"{item}"'
|
|
670
|
+
) # Correctly escape quotes within list strings
|
|
598
671
|
else:
|
|
599
672
|
formatted_items.append(str(item))
|
|
600
673
|
return f"[{', '.join(formatted_items)}]"
|
|
601
674
|
elif isinstance(value, dict):
|
|
602
675
|
if not value:
|
|
603
|
-
return "{}"
|
|
604
|
-
# Format dict as inline table
|
|
676
|
+
return "{{}}"
|
|
605
677
|
formatted_items = []
|
|
606
678
|
for k, v in value.items():
|
|
607
679
|
if isinstance(v, str):
|
|
608
|
-
formatted_items.append(
|
|
680
|
+
formatted_items.append(
|
|
681
|
+
f'{k} = "{v}"'
|
|
682
|
+
) # Correctly escape quotes within dict strings
|
|
609
683
|
else:
|
|
610
684
|
formatted_items.append(f"{k} = {v}")
|
|
611
685
|
return f"{{{', '.join(formatted_items)}}}"
|
|
@@ -613,32 +687,6 @@ def _format_config_value_for_toml(value: Any) -> str:
|
|
|
613
687
|
return str(value)
|
|
614
688
|
|
|
615
689
|
|
|
616
|
-
def _write_json_config_with_comments(
|
|
617
|
-
config_file: Path, config_data: dict[str, Any]
|
|
618
|
-
) -> None:
|
|
619
|
-
"""Write configuration data to a JSON file with formatting."""
|
|
620
|
-
|
|
621
|
-
def convert_for_json(obj: Any) -> Any:
|
|
622
|
-
"""Convert objects to JSON-serializable format."""
|
|
623
|
-
if isinstance(obj, Path):
|
|
624
|
-
return str(obj)
|
|
625
|
-
elif isinstance(obj, dict):
|
|
626
|
-
return {k: convert_for_json(v) for k, v in obj.items()}
|
|
627
|
-
elif isinstance(obj, list):
|
|
628
|
-
return [convert_for_json(item) for item in obj]
|
|
629
|
-
elif hasattr(obj, "__dict__"):
|
|
630
|
-
# Handle complex objects by converting to string
|
|
631
|
-
return str(obj)
|
|
632
|
-
else:
|
|
633
|
-
return obj
|
|
634
|
-
|
|
635
|
-
serializable_data = convert_for_json(config_data)
|
|
636
|
-
|
|
637
|
-
with config_file.open("w", encoding="utf-8") as f:
|
|
638
|
-
json.dump(serializable_data, f, indent=2, sort_keys=True)
|
|
639
|
-
f.write("\n")
|
|
640
|
-
|
|
641
|
-
|
|
642
690
|
def _write_config_file(
|
|
643
691
|
config_file: Path, config_data: dict[str, Any], file_format: str
|
|
644
692
|
) -> None:
|
|
@@ -649,118 +697,3 @@ def _write_config_file(
|
|
|
649
697
|
raise ValueError(
|
|
650
698
|
f"Unsupported config format: {file_format}. Only TOML is supported."
|
|
651
699
|
)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
def _write_toml_config(config_file: Path, config_data: dict[str, Any]) -> None:
|
|
655
|
-
"""Write configuration data to a TOML file with proper formatting."""
|
|
656
|
-
try:
|
|
657
|
-
# Create a nicely formatted TOML file
|
|
658
|
-
with config_file.open("w", encoding="utf-8") as f:
|
|
659
|
-
f.write("# CCProxy API Configuration\n")
|
|
660
|
-
f.write("# Generated by ccproxy config generate-token\n\n")
|
|
661
|
-
|
|
662
|
-
# Write server settings
|
|
663
|
-
if any(
|
|
664
|
-
key in config_data
|
|
665
|
-
for key in ["host", "port", "log_level", "workers", "reload"]
|
|
666
|
-
):
|
|
667
|
-
f.write("# Server configuration\n")
|
|
668
|
-
if "host" in config_data:
|
|
669
|
-
f.write(f'host = "{config_data["host"]}"\n')
|
|
670
|
-
if "port" in config_data:
|
|
671
|
-
f.write(f"port = {config_data['port']}\n")
|
|
672
|
-
if "log_level" in config_data:
|
|
673
|
-
f.write(f'log_level = "{config_data["log_level"]}"\n')
|
|
674
|
-
if "workers" in config_data:
|
|
675
|
-
f.write(f"workers = {config_data['workers']}\n")
|
|
676
|
-
if "reload" in config_data:
|
|
677
|
-
f.write(f"reload = {str(config_data['reload']).lower()}\n")
|
|
678
|
-
f.write("\n")
|
|
679
|
-
|
|
680
|
-
# Write security settings
|
|
681
|
-
if any(key in config_data for key in ["auth_token", "cors_origins"]):
|
|
682
|
-
f.write("# Security configuration\n")
|
|
683
|
-
if "auth_token" in config_data:
|
|
684
|
-
f.write(f'auth_token = "{config_data["auth_token"]}"\n')
|
|
685
|
-
if "cors_origins" in config_data:
|
|
686
|
-
origins = config_data["cors_origins"]
|
|
687
|
-
if isinstance(origins, list):
|
|
688
|
-
origins_str = '", "'.join(origins)
|
|
689
|
-
f.write(f'cors_origins = ["{origins_str}"]\n')
|
|
690
|
-
else:
|
|
691
|
-
f.write(f'cors_origins = ["{origins}"]\n')
|
|
692
|
-
f.write("\n")
|
|
693
|
-
|
|
694
|
-
# Write Claude CLI configuration
|
|
695
|
-
if "claude_cli_path" in config_data:
|
|
696
|
-
f.write("# Claude CLI configuration\n")
|
|
697
|
-
if config_data["claude_cli_path"]:
|
|
698
|
-
f.write(f'claude_cli_path = "{config_data["claude_cli_path"]}"\n')
|
|
699
|
-
else:
|
|
700
|
-
f.write(
|
|
701
|
-
'# claude_cli_path = "/path/to/claude" # Auto-detect if not set\n'
|
|
702
|
-
)
|
|
703
|
-
f.write("\n")
|
|
704
|
-
|
|
705
|
-
# Write Docker settings
|
|
706
|
-
if "docker" in config_data:
|
|
707
|
-
docker_settings = config_data["docker"]
|
|
708
|
-
f.write("# Docker configuration\n")
|
|
709
|
-
f.write("[docker]\n")
|
|
710
|
-
|
|
711
|
-
for key, value in docker_settings.items():
|
|
712
|
-
if isinstance(value, str):
|
|
713
|
-
f.write(f'{key} = "{value}"\n')
|
|
714
|
-
elif isinstance(value, bool):
|
|
715
|
-
f.write(f"{key} = {str(value).lower()}\n")
|
|
716
|
-
elif isinstance(value, int | float):
|
|
717
|
-
f.write(f"{key} = {value}\n")
|
|
718
|
-
elif isinstance(value, list):
|
|
719
|
-
if value: # Only write non-empty lists
|
|
720
|
-
if all(isinstance(item, str) for item in value):
|
|
721
|
-
items_str = '", "'.join(value)
|
|
722
|
-
f.write(f'{key} = ["{items_str}"]\n')
|
|
723
|
-
else:
|
|
724
|
-
f.write(f"{key} = {value}\n")
|
|
725
|
-
else:
|
|
726
|
-
f.write(f"{key} = []\n")
|
|
727
|
-
elif isinstance(value, dict):
|
|
728
|
-
if value: # Only write non-empty dicts
|
|
729
|
-
f.write(f"{key} = {json.dumps(value)}\n")
|
|
730
|
-
else:
|
|
731
|
-
f.write(f"{key} = {{}}\n")
|
|
732
|
-
elif value is None:
|
|
733
|
-
f.write(f"# {key} = null # Not configured\n")
|
|
734
|
-
f.write("\n")
|
|
735
|
-
|
|
736
|
-
# Write any remaining top-level settings
|
|
737
|
-
written_keys = {
|
|
738
|
-
"host",
|
|
739
|
-
"port",
|
|
740
|
-
"log_level",
|
|
741
|
-
"workers",
|
|
742
|
-
"reload",
|
|
743
|
-
"auth_token",
|
|
744
|
-
"cors_origins",
|
|
745
|
-
"claude_cli_path",
|
|
746
|
-
"docker",
|
|
747
|
-
}
|
|
748
|
-
remaining_keys = set(config_data.keys()) - written_keys
|
|
749
|
-
|
|
750
|
-
if remaining_keys:
|
|
751
|
-
f.write("# Additional settings\n")
|
|
752
|
-
for key in sorted(remaining_keys):
|
|
753
|
-
value = config_data[key]
|
|
754
|
-
if isinstance(value, str):
|
|
755
|
-
f.write(f'{key} = "{value}"\n')
|
|
756
|
-
elif isinstance(value, bool):
|
|
757
|
-
f.write(f"{key} = {str(value).lower()}\n")
|
|
758
|
-
elif isinstance(value, int | float):
|
|
759
|
-
f.write(f"{key} = {value}\n")
|
|
760
|
-
elif isinstance(value, list | dict):
|
|
761
|
-
f.write(f"{key} = {json.dumps(value)}\n")
|
|
762
|
-
elif value is None:
|
|
763
|
-
f.write(f"# {key} = null\n")
|
|
764
|
-
|
|
765
|
-
except Exception as e:
|
|
766
|
-
raise ValueError(f"Failed to write TOML configuration: {e}") from e
|