ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/core/logging.py
CHANGED
|
@@ -1,24 +1,277 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
2
5
|
import shutil
|
|
3
6
|
import sys
|
|
4
7
|
from collections.abc import MutableMapping
|
|
5
8
|
from pathlib import Path
|
|
6
|
-
from typing import Any, TextIO
|
|
9
|
+
from typing import Any, Protocol, TextIO
|
|
7
10
|
|
|
8
11
|
import structlog
|
|
9
12
|
from rich.console import Console
|
|
10
13
|
from rich.traceback import Traceback
|
|
14
|
+
from structlog.contextvars import bind_contextvars
|
|
11
15
|
from structlog.stdlib import BoundLogger
|
|
12
16
|
from structlog.typing import ExcInfo, Processor
|
|
13
17
|
|
|
18
|
+
from ccproxy.core.id_utils import generate_short_id
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_LOG_LEVEL_NAME = "WARNING"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Custom protocol for BoundLogger with trace method
|
|
25
|
+
class TraceBoundLogger(Protocol):
|
|
26
|
+
"""Protocol defining BoundLogger with trace method."""
|
|
27
|
+
|
|
28
|
+
def trace(self, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
29
|
+
"""Log at TRACE level."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def debug(self, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
33
|
+
"""Log at DEBUG level."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def info(self, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
37
|
+
"""Log at INFO level."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def warning(self, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
41
|
+
"""Log at WARNING level."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def error(self, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
45
|
+
"""Log at ERROR level."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def bind(self, **kwargs: Any) -> "TraceBoundLogger":
|
|
49
|
+
"""Bind additional context to logger."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
53
|
+
"""Log at specific level."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Import LogCategory locally to avoid circular import
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Add TRACE level below DEBUG
|
|
61
|
+
TRACE_LEVEL = 5
|
|
62
|
+
logging.addLevelName(TRACE_LEVEL, "TRACE")
|
|
63
|
+
|
|
64
|
+
# Register TRACE level with structlog
|
|
65
|
+
structlog.stdlib.LEVEL_TO_NAME[TRACE_LEVEL] = "trace" # type: ignore[attr-defined]
|
|
66
|
+
structlog.stdlib.NAME_TO_LEVEL["trace"] = TRACE_LEVEL # type: ignore[attr-defined]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Monkey-patch trace method to Logger class
|
|
70
|
+
def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
|
|
71
|
+
"""Log at TRACE level (below DEBUG)."""
|
|
72
|
+
if self.isEnabledFor(TRACE_LEVEL):
|
|
73
|
+
self._log(TRACE_LEVEL, message, args, **kwargs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
logging.Logger.trace = trace # type: ignore[attr-defined]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Custom BoundLogger that includes trace method
|
|
80
|
+
class TraceBoundLoggerImpl(BoundLogger):
|
|
81
|
+
"""BoundLogger with trace method support."""
|
|
82
|
+
|
|
83
|
+
def trace(self, msg: str, *args: Any, **kwargs: Any) -> Any:
|
|
84
|
+
"""Log at TRACE level."""
|
|
85
|
+
return self.log(TRACE_LEVEL, msg, *args, **kwargs)
|
|
86
|
+
|
|
14
87
|
|
|
15
88
|
suppress_debug = [
|
|
16
89
|
"ccproxy.scheduler",
|
|
17
|
-
"ccproxy.observability.context",
|
|
18
|
-
"ccproxy.utils.simple_request_logger",
|
|
19
90
|
]
|
|
20
91
|
|
|
21
92
|
|
|
93
|
+
def category_filter(
|
|
94
|
+
logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
|
|
95
|
+
) -> MutableMapping[str, Any]:
|
|
96
|
+
"""Filter logs by category based on environment configuration."""
|
|
97
|
+
# Get filter settings from environment
|
|
98
|
+
included_channels = os.getenv("CCPROXY_LOG_CHANNELS", "").strip()
|
|
99
|
+
excluded_channels = os.getenv("CCPROXY_LOG_EXCLUDE_CHANNELS", "").strip()
|
|
100
|
+
|
|
101
|
+
if not included_channels and not excluded_channels:
|
|
102
|
+
return event_dict # No filtering
|
|
103
|
+
|
|
104
|
+
included = (
|
|
105
|
+
[c.strip() for c in included_channels.split(",") if c.strip()]
|
|
106
|
+
if included_channels
|
|
107
|
+
else []
|
|
108
|
+
)
|
|
109
|
+
excluded = (
|
|
110
|
+
[c.strip() for c in excluded_channels.split(",") if c.strip()]
|
|
111
|
+
if excluded_channels
|
|
112
|
+
else []
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
category = event_dict.get("category")
|
|
116
|
+
|
|
117
|
+
# For foreign (stdlib) logs without category, check if logger name suggests a category
|
|
118
|
+
if category is None:
|
|
119
|
+
logger_name = event_dict.get("logger", "")
|
|
120
|
+
# Map common logger names to categories
|
|
121
|
+
if logger_name.startswith(("uvicorn", "fastapi", "starlette")):
|
|
122
|
+
category = "general" # Allow uvicorn/fastapi logs through as general
|
|
123
|
+
elif logger_name.startswith("httpx"):
|
|
124
|
+
category = "http"
|
|
125
|
+
else:
|
|
126
|
+
category = "general" # Default fallback
|
|
127
|
+
|
|
128
|
+
# Add the category to the event dict for consistent handling
|
|
129
|
+
event_dict["category"] = category
|
|
130
|
+
|
|
131
|
+
# Apply filters - be more permissive with foreign logs that got "general" as fallback
|
|
132
|
+
# and ALWAYS allow errors and warnings through regardless of category filtering
|
|
133
|
+
log_level = event_dict.get("level", "").lower()
|
|
134
|
+
is_critical_message = log_level in ("error", "warning", "critical")
|
|
135
|
+
|
|
136
|
+
if included and category not in included:
|
|
137
|
+
# Always allow critical messages through regardless of category filtering
|
|
138
|
+
if is_critical_message:
|
|
139
|
+
return event_dict
|
|
140
|
+
|
|
141
|
+
# If it's a foreign log with "general" fallback, and "general" is not in included channels,
|
|
142
|
+
# still allow it through to prevent breaking stdlib logging
|
|
143
|
+
logger_name = event_dict.get("logger", "")
|
|
144
|
+
is_foreign_log = not logger_name.startswith(
|
|
145
|
+
"ccproxy"
|
|
146
|
+
) and not logger_name.startswith("plugins")
|
|
147
|
+
|
|
148
|
+
if not (is_foreign_log and category == "general"):
|
|
149
|
+
raise structlog.DropEvent
|
|
150
|
+
|
|
151
|
+
if excluded and category in excluded:
|
|
152
|
+
# Always allow critical messages through even if their category is explicitly excluded
|
|
153
|
+
if is_critical_message:
|
|
154
|
+
return event_dict
|
|
155
|
+
raise structlog.DropEvent
|
|
156
|
+
|
|
157
|
+
return event_dict
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def format_category_for_console(
|
|
161
|
+
logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
|
|
162
|
+
) -> MutableMapping[str, Any]:
|
|
163
|
+
"""Format category field for better visibility in console output."""
|
|
164
|
+
logger_name = event_dict.get("logger", "") or ""
|
|
165
|
+
category = event_dict.get("category")
|
|
166
|
+
event = event_dict.get("event", "")
|
|
167
|
+
|
|
168
|
+
# Treat non-ccproxy/plugin loggers as external for display purposes.
|
|
169
|
+
is_external_logger = not (
|
|
170
|
+
logger_name.startswith("ccproxy") or logger_name.startswith("plugins")
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if category:
|
|
174
|
+
category_upper = str(category).upper()
|
|
175
|
+
|
|
176
|
+
# Avoid echoing redundant [GENERAL] prefixes for external libraries.
|
|
177
|
+
if not (category_upper == "GENERAL" and is_external_logger):
|
|
178
|
+
event_dict["event"] = f"[{category_upper}] {event}"
|
|
179
|
+
else:
|
|
180
|
+
# Add default category if missing.
|
|
181
|
+
event_dict["category"] = "general"
|
|
182
|
+
if not is_external_logger:
|
|
183
|
+
event_dict["event"] = f"[GENERAL] {event}"
|
|
184
|
+
|
|
185
|
+
return event_dict
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class CategoryConsoleRenderer:
|
|
189
|
+
"""Custom console renderer that formats categories as a separate padded column."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, base_renderer: Any):
|
|
192
|
+
self.base_renderer = base_renderer
|
|
193
|
+
|
|
194
|
+
def __call__(
|
|
195
|
+
self, logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
|
|
196
|
+
) -> str:
|
|
197
|
+
# Extract category and plugin_name, remove from event dict to prevent duplicate display
|
|
198
|
+
category = event_dict.pop("category", "general")
|
|
199
|
+
plugin_name = event_dict.pop("plugin_name", None)
|
|
200
|
+
|
|
201
|
+
# Get the rendered output from base renderer (without category/plugin_name in key-value pairs)
|
|
202
|
+
rendered = self.base_renderer(logger, method_name, event_dict)
|
|
203
|
+
|
|
204
|
+
# Color mapping for different categories
|
|
205
|
+
category_colors = {
|
|
206
|
+
"lifecycle": "\033[92m", # bright green
|
|
207
|
+
"plugin": "\033[94m", # bright blue
|
|
208
|
+
"http": "\033[95m", # bright magenta
|
|
209
|
+
"streaming": "\033[96m", # bright cyan
|
|
210
|
+
"auth": "\033[93m", # bright yellow
|
|
211
|
+
"transform": "\033[91m", # bright red
|
|
212
|
+
"cache": "\033[97m", # bright white
|
|
213
|
+
"middleware": "\033[35m", # magenta
|
|
214
|
+
"config": "\033[34m", # blue
|
|
215
|
+
"metrics": "\033[32m", # green
|
|
216
|
+
"access": "\033[33m", # yellow
|
|
217
|
+
"request": "\033[36m", # cyan
|
|
218
|
+
"general": "\033[37m", # white
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Plugin name colors (distinct from categories)
|
|
222
|
+
plugin_colors = {
|
|
223
|
+
"claude_api": "\033[38;5;33m", # blue
|
|
224
|
+
"claude_sdk": "\033[38;5;39m", # bright blue
|
|
225
|
+
"codex": "\033[38;5;214m", # orange
|
|
226
|
+
"permissions": "\033[38;5;165m", # purple
|
|
227
|
+
"raw_http_logger": "\033[38;5;150m", # light green
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Get colors
|
|
231
|
+
category_color = category_colors.get(category.lower(), "\033[37m")
|
|
232
|
+
plugin_color = (
|
|
233
|
+
plugin_colors.get(plugin_name, "\033[38;5;242m") if plugin_name else None
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Build the display fields
|
|
237
|
+
# Truncate long category names to fit the field width
|
|
238
|
+
truncated_category = (
|
|
239
|
+
category.lower()[:10] if len(category) > 10 else category.lower()
|
|
240
|
+
)
|
|
241
|
+
category_field = f"{category_color}\033[1m[{truncated_category:<10}]\033[0m"
|
|
242
|
+
|
|
243
|
+
# Always show a plugin field - either plugin name or "core"
|
|
244
|
+
if plugin_name:
|
|
245
|
+
# Truncate long plugin names to fit the field width
|
|
246
|
+
truncated_name = plugin_name[:12] if len(plugin_name) > 12 else plugin_name
|
|
247
|
+
plugin_field = f"{plugin_color}\033[1m[{truncated_name:<12}]\033[0m "
|
|
248
|
+
else:
|
|
249
|
+
# Show "core" for non-plugin logs with a distinct color
|
|
250
|
+
core_color = "\033[38;5;8m" # dark gray
|
|
251
|
+
plugin_field = f"{core_color}\033[1m[{'core':<12}]\033[0m "
|
|
252
|
+
|
|
253
|
+
# Insert fields after the level field in the rendered output
|
|
254
|
+
# Find the position right after the level field closes with "] "
|
|
255
|
+
level_end_pattern = r"(\[[^\]]*\[[^\]]*m[^\]]*\[[^\]]*m\])\s+"
|
|
256
|
+
match = re.search(level_end_pattern, rendered)
|
|
257
|
+
|
|
258
|
+
if match:
|
|
259
|
+
# Insert plugin_field and category_field after the level field
|
|
260
|
+
insert_pos = match.end()
|
|
261
|
+
rendered = (
|
|
262
|
+
rendered[:insert_pos]
|
|
263
|
+
+ plugin_field
|
|
264
|
+
+ category_field
|
|
265
|
+
+ " "
|
|
266
|
+
+ rendered[insert_pos:]
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
# Fallback: prepend fields to the beginning
|
|
270
|
+
rendered = plugin_field + category_field + " " + rendered
|
|
271
|
+
|
|
272
|
+
return str(rendered)
|
|
273
|
+
|
|
274
|
+
|
|
22
275
|
def configure_structlog(log_level: int = logging.INFO) -> None:
|
|
23
276
|
"""Configure structlog with shared processors following canonical pattern."""
|
|
24
277
|
# Shared processors for all structlog loggers
|
|
@@ -27,6 +280,7 @@ def configure_structlog(log_level: int = logging.INFO) -> None:
|
|
|
27
280
|
structlog.stdlib.filter_by_level,
|
|
28
281
|
structlog.stdlib.add_log_level,
|
|
29
282
|
structlog.stdlib.add_logger_name,
|
|
283
|
+
category_filter, # Add category filtering
|
|
30
284
|
]
|
|
31
285
|
|
|
32
286
|
# Add debug-specific processors
|
|
@@ -75,7 +329,7 @@ def configure_structlog(log_level: int = logging.INFO) -> None:
|
|
|
75
329
|
processors=processors,
|
|
76
330
|
context_class=dict,
|
|
77
331
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
78
|
-
wrapper_class=
|
|
332
|
+
wrapper_class=TraceBoundLoggerImpl,
|
|
79
333
|
cache_logger_on_first_use=True,
|
|
80
334
|
)
|
|
81
335
|
|
|
@@ -112,12 +366,17 @@ def setup_logging(
|
|
|
112
366
|
json_logs: bool = False,
|
|
113
367
|
log_level_name: str = "DEBUG",
|
|
114
368
|
log_file: str | None = None,
|
|
115
|
-
) ->
|
|
369
|
+
) -> TraceBoundLogger:
|
|
116
370
|
"""
|
|
117
371
|
Setup logging for the entire application using canonical structlog pattern.
|
|
118
372
|
Returns a structlog logger instance.
|
|
119
373
|
"""
|
|
120
|
-
|
|
374
|
+
# Handle custom TRACE level explicitly
|
|
375
|
+
log_level_upper = log_level_name.upper()
|
|
376
|
+
if log_level_upper == "TRACE":
|
|
377
|
+
log_level = TRACE_LEVEL
|
|
378
|
+
else:
|
|
379
|
+
log_level = getattr(logging, log_level_upper, logging.INFO)
|
|
121
380
|
|
|
122
381
|
# Install rich traceback handler globally with frame limit
|
|
123
382
|
# install_rich_traceback(
|
|
@@ -149,6 +408,7 @@ def setup_logging(
|
|
|
149
408
|
structlog.contextvars.merge_contextvars,
|
|
150
409
|
structlog.stdlib.add_log_level,
|
|
151
410
|
structlog.stdlib.add_logger_name,
|
|
411
|
+
category_filter, # Apply category filtering to all logs
|
|
152
412
|
structlog.dev.set_exc_info,
|
|
153
413
|
]
|
|
154
414
|
|
|
@@ -189,16 +449,25 @@ def setup_logging(
|
|
|
189
449
|
# 4. Setup console handler with ConsoleRenderer
|
|
190
450
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
191
451
|
console_handler.setLevel(log_level)
|
|
452
|
+
base_console_renderer = structlog.dev.ConsoleRenderer(
|
|
453
|
+
exception_formatter=rich_traceback, # Use rich for better formatting
|
|
454
|
+
colors=True,
|
|
455
|
+
pad_event=30,
|
|
456
|
+
)
|
|
457
|
+
|
|
192
458
|
console_renderer = (
|
|
193
459
|
structlog.processors.JSONRenderer()
|
|
194
460
|
if json_logs
|
|
195
|
-
else
|
|
196
|
-
exception_formatter=rich_traceback # structlog.dev.rich_traceback, # Use rich for better formatting
|
|
197
|
-
)
|
|
461
|
+
else CategoryConsoleRenderer(base_console_renderer)
|
|
198
462
|
)
|
|
199
463
|
|
|
200
464
|
# Console gets human-readable timestamps for both structlog and stdlib logs
|
|
201
|
-
|
|
465
|
+
# Note: format_category_for_console must come after category_filter
|
|
466
|
+
console_processors = shared_processors + [
|
|
467
|
+
console_timestamper,
|
|
468
|
+
format_timestamp_ms,
|
|
469
|
+
format_category_for_console,
|
|
470
|
+
]
|
|
202
471
|
console_handler.setFormatter(
|
|
203
472
|
structlog.stdlib.ProcessorFormatter(
|
|
204
473
|
foreign_pre_chain=console_processors, # type: ignore[arg-type]
|
|
@@ -259,29 +528,215 @@ def setup_logging(
|
|
|
259
528
|
"urllib3",
|
|
260
529
|
"urllib3.connectionpool",
|
|
261
530
|
"requests",
|
|
262
|
-
"aiohttp",
|
|
263
531
|
"httpcore",
|
|
264
532
|
"httpcore.http11",
|
|
265
533
|
"fastapi_mcp",
|
|
266
534
|
"sse_starlette",
|
|
267
535
|
"mcp",
|
|
536
|
+
"hpack",
|
|
268
537
|
]:
|
|
269
538
|
noisy_logger = logging.getLogger(noisy_logger_name)
|
|
270
539
|
noisy_logger.handlers = []
|
|
271
540
|
noisy_logger.propagate = True
|
|
272
541
|
noisy_logger.setLevel(noisy_log_level)
|
|
273
542
|
|
|
274
|
-
|
|
543
|
+
for logger_name in suppress_debug:
|
|
275
544
|
logging.getLogger(logger_name).setLevel(
|
|
276
545
|
logging.INFO if log_level <= logging.DEBUG else log_level
|
|
277
|
-
)
|
|
278
|
-
for logger_name in suppress_debug
|
|
279
|
-
]
|
|
546
|
+
)
|
|
280
547
|
|
|
281
548
|
return structlog.get_logger() # type: ignore[no-any-return]
|
|
282
549
|
|
|
283
550
|
|
|
284
551
|
# Create a convenience function for getting loggers
|
|
285
|
-
def get_logger(name: str | None = None) ->
|
|
286
|
-
"""Get a structlog logger instance.
|
|
287
|
-
|
|
552
|
+
def get_logger(name: str | None = None) -> TraceBoundLogger:
|
|
553
|
+
"""Get a structlog logger instance with request context automatically bound.
|
|
554
|
+
|
|
555
|
+
This function checks for an active RequestContext and automatically binds
|
|
556
|
+
the request_id to the logger if available, ensuring all logs are correlated
|
|
557
|
+
with the current request.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
name: Logger name (typically __name__)
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
TraceBoundLogger with request_id bound if available
|
|
564
|
+
"""
|
|
565
|
+
logger = structlog.get_logger(name)
|
|
566
|
+
|
|
567
|
+
# Try to get request context and bind request_id if available
|
|
568
|
+
try:
|
|
569
|
+
from ccproxy.core.request_context import RequestContext
|
|
570
|
+
|
|
571
|
+
context = RequestContext.get_current()
|
|
572
|
+
if context and context.request_id:
|
|
573
|
+
logger = logger.bind(request_id=context.request_id)
|
|
574
|
+
except Exception:
|
|
575
|
+
# If anything fails, just return the regular logger
|
|
576
|
+
# This ensures backward compatibility
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
return logger # type: ignore[no-any-return]
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def get_plugin_logger(name: str | None = None) -> TraceBoundLogger:
|
|
583
|
+
"""Get a plugin-aware logger with plugin_name automatically bound.
|
|
584
|
+
|
|
585
|
+
This function auto-detects the plugin name from the caller's module path
|
|
586
|
+
and binds it to the logger. Preserves all existing functionality including
|
|
587
|
+
request_id binding and trace method.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
name: Logger name (auto-detected from caller if None)
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
TraceBoundLogger with plugin_name and request_id bound if available
|
|
594
|
+
"""
|
|
595
|
+
if name is None:
|
|
596
|
+
# Auto-detect caller's module name
|
|
597
|
+
frame = inspect.currentframe()
|
|
598
|
+
if frame and frame.f_back:
|
|
599
|
+
name = frame.f_back.f_globals.get("__name__", "unknown")
|
|
600
|
+
else:
|
|
601
|
+
name = "unknown"
|
|
602
|
+
|
|
603
|
+
# Use existing get_logger (preserves request_id binding & trace method)
|
|
604
|
+
logger = get_logger(name)
|
|
605
|
+
|
|
606
|
+
# Extract and bind plugin name for plugin modules
|
|
607
|
+
if name and name.startswith("plugins."):
|
|
608
|
+
parts = name.split(".", 2)
|
|
609
|
+
if len(parts) > 1:
|
|
610
|
+
plugin_name = parts[1] # e.g., "claude_api", "codex"
|
|
611
|
+
logger = logger.bind(plugin_name=plugin_name)
|
|
612
|
+
|
|
613
|
+
return logger
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _parse_arg_value(argv: list[str], flag: str) -> str | None:
|
|
617
|
+
"""Parse a simple CLI flag value from argv.
|
|
618
|
+
|
|
619
|
+
Supports "--flag value" and "--flag=value" forms. Returns None if not present.
|
|
620
|
+
"""
|
|
621
|
+
if not argv:
|
|
622
|
+
return None
|
|
623
|
+
try:
|
|
624
|
+
for i, token in enumerate(argv):
|
|
625
|
+
if token == flag and i + 1 < len(argv):
|
|
626
|
+
return argv[i + 1]
|
|
627
|
+
if token.startswith(flag + "="):
|
|
628
|
+
return token.split("=", 1)[1]
|
|
629
|
+
except Exception:
|
|
630
|
+
# Be forgiving in bootstrap parsing
|
|
631
|
+
return None
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def bootstrap_cli_logging(argv: list[str] | None = None) -> None:
|
|
636
|
+
"""Best-effort early logging setup from env and CLI args.
|
|
637
|
+
|
|
638
|
+
- Parses `--log-level` and `--log-file` from argv (if provided).
|
|
639
|
+
- Honors env overrides `LOGGING__LEVEL`, `LOGGING__FILE`.
|
|
640
|
+
- Enables JSON logs if explicitly requested via `LOGGING__FORMAT=json` or `CCPROXY_JSON_LOGS=true`.
|
|
641
|
+
- No-op if structlog is already configured, letting later setup prevail.
|
|
642
|
+
|
|
643
|
+
This is intentionally lightweight and is followed by a full `setup_logging`
|
|
644
|
+
call after settings are loaded (e.g., in the serve command), so runtime
|
|
645
|
+
changes from config are still applied.
|
|
646
|
+
"""
|
|
647
|
+
try:
|
|
648
|
+
if structlog.is_configured():
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
if argv is None:
|
|
652
|
+
argv = sys.argv[1:]
|
|
653
|
+
|
|
654
|
+
# Env-based defaults
|
|
655
|
+
env_level = os.getenv("LOGGING__LEVEL") or os.getenv("CCPROXY_LOG_LEVEL")
|
|
656
|
+
env_file = os.getenv("LOGGING__FILE")
|
|
657
|
+
env_format = os.getenv("LOGGING__FORMAT")
|
|
658
|
+
|
|
659
|
+
# CLI overrides
|
|
660
|
+
arg_level = _parse_arg_value(argv, "--log-level")
|
|
661
|
+
arg_file = _parse_arg_value(argv, "--log-file")
|
|
662
|
+
|
|
663
|
+
# We always want a predictable, quiet baseline before full config.
|
|
664
|
+
# Default to INFO unless an explicit override requests another level.
|
|
665
|
+
# Resolve effective values (CLI > env)
|
|
666
|
+
level = (arg_level or env_level or DEFAULT_LOG_LEVEL_NAME).upper()
|
|
667
|
+
log_file = arg_file or env_file
|
|
668
|
+
|
|
669
|
+
# JSON if explicitly requested via env
|
|
670
|
+
json_logs = False
|
|
671
|
+
if env_format:
|
|
672
|
+
json_logs = env_format.lower() == "json"
|
|
673
|
+
|
|
674
|
+
# Apply early setup. Safe to run again later with final settings.
|
|
675
|
+
# Never escalate to DEBUG/TRACE unless explicitly requested via env/argv.
|
|
676
|
+
if level not in {"TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}:
|
|
677
|
+
level = DEFAULT_LOG_LEVEL_NAME
|
|
678
|
+
setup_logging(json_logs=json_logs, log_level_name=level, log_file=log_file)
|
|
679
|
+
except Exception:
|
|
680
|
+
# Never break CLI due to bootstrap; final setup will run later.
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def set_command_context(cmd_id: str | None = None) -> str:
|
|
685
|
+
"""Bind a command-wide correlation ID to structlog context.
|
|
686
|
+
|
|
687
|
+
Uses structlog.contextvars so all logs (including from plugins) will carry
|
|
688
|
+
`cmd_id` once logging is configured with `merge_contextvars`.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
cmd_id: Optional explicit command ID. If None, a UUID4 is generated.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
The command ID that was bound.
|
|
695
|
+
"""
|
|
696
|
+
try:
|
|
697
|
+
if not cmd_id:
|
|
698
|
+
cmd_id = generate_short_id()
|
|
699
|
+
# Bind only cmd_id to avoid colliding with per-request request_id fields
|
|
700
|
+
bind_contextvars(cmd_id=cmd_id)
|
|
701
|
+
return cmd_id
|
|
702
|
+
except Exception:
|
|
703
|
+
# Be defensive: never break CLI startup due to context binding
|
|
704
|
+
return cmd_id or ""
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# --- Lightweight test-time bootstrap ---------------------------------------
|
|
708
|
+
# Ensure structlog logs are capturable by pytest's caplog without requiring
|
|
709
|
+
# full application setup. When running under pytest (PYTEST_CURRENT_TEST),
|
|
710
|
+
# configure structlog to emit through stdlib logging with a simple renderer
|
|
711
|
+
# and set the root level to INFO so info logs are not filtered.
|
|
712
|
+
def _bootstrap_test_logging_if_needed() -> None:
|
|
713
|
+
try:
|
|
714
|
+
if os.getenv("PYTEST_CURRENT_TEST") and not structlog.is_configured():
|
|
715
|
+
# Ensure INFO-level logs are visible to caplog
|
|
716
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
717
|
+
|
|
718
|
+
# Configure structlog to hand off to stdlib with extra fields so that
|
|
719
|
+
# pytest's caplog sees attributes like `record.category`.
|
|
720
|
+
structlog.configure(
|
|
721
|
+
processors=[
|
|
722
|
+
structlog.stdlib.filter_by_level,
|
|
723
|
+
structlog.stdlib.add_log_level,
|
|
724
|
+
structlog.stdlib.add_logger_name,
|
|
725
|
+
category_filter,
|
|
726
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
727
|
+
structlog.processors.format_exc_info,
|
|
728
|
+
# Pass fields as LogRecord.extra for caplog
|
|
729
|
+
structlog.stdlib.render_to_log_kwargs,
|
|
730
|
+
],
|
|
731
|
+
context_class=dict,
|
|
732
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
733
|
+
wrapper_class=TraceBoundLoggerImpl,
|
|
734
|
+
cache_logger_on_first_use=True,
|
|
735
|
+
)
|
|
736
|
+
except Exception:
|
|
737
|
+
# Never fail test imports due to logging bootstrap
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# Invoke test bootstrap on import if appropriate
|
|
742
|
+
_bootstrap_test_logging_if_needed()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""CCProxy Plugin System public API (minimal re-exports).
|
|
2
|
+
|
|
3
|
+
This module exposes the common symbols used by plugins and app code while
|
|
4
|
+
keeping imports straightforward to avoid circular dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .declaration import (
|
|
8
|
+
AuthCommandSpec,
|
|
9
|
+
FormatAdapterSpec,
|
|
10
|
+
FormatPair,
|
|
11
|
+
HookSpec,
|
|
12
|
+
MiddlewareLayer,
|
|
13
|
+
MiddlewareSpec,
|
|
14
|
+
PluginContext,
|
|
15
|
+
PluginManifest,
|
|
16
|
+
PluginRuntimeProtocol,
|
|
17
|
+
RouteSpec,
|
|
18
|
+
TaskSpec,
|
|
19
|
+
)
|
|
20
|
+
from .factories import (
|
|
21
|
+
BaseProviderPluginFactory,
|
|
22
|
+
PluginRegistry,
|
|
23
|
+
)
|
|
24
|
+
from .interfaces import (
|
|
25
|
+
AuthProviderPluginFactory,
|
|
26
|
+
BasePluginFactory,
|
|
27
|
+
PluginFactory,
|
|
28
|
+
ProviderPluginFactory,
|
|
29
|
+
SystemPluginFactory,
|
|
30
|
+
factory_type_name,
|
|
31
|
+
)
|
|
32
|
+
from .loader import load_cli_plugins, load_plugin_system
|
|
33
|
+
from .middleware import CoreMiddlewareSpec, MiddlewareManager, setup_default_middleware
|
|
34
|
+
from .runtime import (
|
|
35
|
+
AuthProviderPluginRuntime,
|
|
36
|
+
BasePluginRuntime,
|
|
37
|
+
ProviderPluginRuntime,
|
|
38
|
+
SystemPluginRuntime,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Declarations
|
|
44
|
+
"PluginManifest",
|
|
45
|
+
"PluginContext",
|
|
46
|
+
"PluginRuntimeProtocol",
|
|
47
|
+
"MiddlewareSpec",
|
|
48
|
+
"MiddlewareLayer",
|
|
49
|
+
"RouteSpec",
|
|
50
|
+
"TaskSpec",
|
|
51
|
+
"HookSpec",
|
|
52
|
+
"AuthCommandSpec",
|
|
53
|
+
"FormatAdapterSpec",
|
|
54
|
+
"FormatPair",
|
|
55
|
+
# Runtime
|
|
56
|
+
"BasePluginRuntime",
|
|
57
|
+
"SystemPluginRuntime",
|
|
58
|
+
"ProviderPluginRuntime",
|
|
59
|
+
"AuthProviderPluginRuntime",
|
|
60
|
+
# Base factory
|
|
61
|
+
"BaseProviderPluginFactory",
|
|
62
|
+
# Factory and registry
|
|
63
|
+
"PluginFactory",
|
|
64
|
+
"BasePluginFactory",
|
|
65
|
+
"SystemPluginFactory",
|
|
66
|
+
"ProviderPluginFactory",
|
|
67
|
+
"AuthProviderPluginFactory",
|
|
68
|
+
"PluginRegistry",
|
|
69
|
+
"factory_type_name",
|
|
70
|
+
# Middleware
|
|
71
|
+
"MiddlewareManager",
|
|
72
|
+
"CoreMiddlewareSpec",
|
|
73
|
+
"setup_default_middleware",
|
|
74
|
+
# Loader functions
|
|
75
|
+
"load_plugin_system",
|
|
76
|
+
"load_cli_plugins",
|
|
77
|
+
]
|