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
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Core HTTP request tracer hook implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
from ccproxy.core.plugins.hooks import Hook
|
|
10
|
+
from ccproxy.core.plugins.hooks.base import HookContext
|
|
11
|
+
from ccproxy.core.plugins.hooks.events import HookEvent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HTTPTracerHook(Hook):
|
|
18
|
+
"""Core hook for tracing all HTTP requests and responses.
|
|
19
|
+
|
|
20
|
+
This hook captures HTTP_REQUEST, HTTP_RESPONSE, and HTTP_ERROR events
|
|
21
|
+
for both client-side (CCProxy → providers) and server-side (client → CCProxy)
|
|
22
|
+
HTTP traffic. It uses injected formatters for consistent logging.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name = "core_http_tracer"
|
|
26
|
+
events = [
|
|
27
|
+
HookEvent.HTTP_REQUEST,
|
|
28
|
+
HookEvent.HTTP_RESPONSE,
|
|
29
|
+
HookEvent.HTTP_ERROR,
|
|
30
|
+
]
|
|
31
|
+
priority = 100 # Run early to capture raw data
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
json_formatter: Any = None,
|
|
36
|
+
raw_formatter: Any = None,
|
|
37
|
+
enabled: bool = True,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the HTTP tracer hook.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
json_formatter: JSONFormatter instance for structured logging
|
|
43
|
+
raw_formatter: RawHTTPFormatter instance for raw HTTP logging
|
|
44
|
+
enabled: Whether the hook is enabled
|
|
45
|
+
"""
|
|
46
|
+
self.enabled = enabled
|
|
47
|
+
self.json_formatter = json_formatter
|
|
48
|
+
self.raw_formatter = raw_formatter
|
|
49
|
+
|
|
50
|
+
if self.enabled:
|
|
51
|
+
logger.debug(
|
|
52
|
+
"core_http_tracer_hook_initialized",
|
|
53
|
+
json_logs=json_formatter is not None,
|
|
54
|
+
raw_http=raw_formatter is not None,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def __call__(self, context: HookContext) -> None:
|
|
58
|
+
"""Process HTTP events and log them.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
context: Hook context with event data
|
|
62
|
+
"""
|
|
63
|
+
if not self.enabled:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
event = context.event
|
|
67
|
+
try:
|
|
68
|
+
if event == HookEvent.HTTP_REQUEST:
|
|
69
|
+
await self._log_http_request(context)
|
|
70
|
+
elif event == HookEvent.HTTP_RESPONSE:
|
|
71
|
+
await self._log_http_response(context)
|
|
72
|
+
elif event == HookEvent.HTTP_ERROR:
|
|
73
|
+
await self._log_http_error(context)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(
|
|
76
|
+
"core_http_tracer_hook_error",
|
|
77
|
+
hook_event=event.value if hasattr(event, "value") else str(event),
|
|
78
|
+
error=str(e),
|
|
79
|
+
exc_info=e,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def _log_http_request(self, context: HookContext) -> None:
|
|
83
|
+
"""Log an HTTP request.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
context: Hook context with request data
|
|
87
|
+
"""
|
|
88
|
+
method = context.data.get("method", "UNKNOWN")
|
|
89
|
+
url = context.data.get("url", "")
|
|
90
|
+
headers_any = context.data.get("headers", {})
|
|
91
|
+
headers_pairs = self._normalize_header_pairs(headers_any)
|
|
92
|
+
body = context.data.get("body")
|
|
93
|
+
is_json = context.data.get("is_json", False)
|
|
94
|
+
|
|
95
|
+
# Use existing request ID from context or generate new one
|
|
96
|
+
request_id = (
|
|
97
|
+
context.data.get("request_id")
|
|
98
|
+
or context.metadata.get("request_id")
|
|
99
|
+
or str(uuid.uuid4())
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Store request ID in context for response correlation
|
|
103
|
+
context.data["request_id"] = request_id
|
|
104
|
+
|
|
105
|
+
# Determine if this is a provider request
|
|
106
|
+
# First check explicit context markers, then fall back to URL analysis
|
|
107
|
+
if context.data.get("is_provider_request"):
|
|
108
|
+
is_provider_request = True
|
|
109
|
+
elif context.data.get("is_client_request"):
|
|
110
|
+
is_provider_request = False
|
|
111
|
+
else:
|
|
112
|
+
# Fall back to URL analysis for backward compatibility
|
|
113
|
+
is_provider_request = self._is_provider_request(url)
|
|
114
|
+
|
|
115
|
+
logger.debug(
|
|
116
|
+
"core_http_request",
|
|
117
|
+
request_id=request_id,
|
|
118
|
+
method=method,
|
|
119
|
+
url=url,
|
|
120
|
+
is_provider_request=is_provider_request,
|
|
121
|
+
headers=headers_pairs,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Log with JSON formatter
|
|
125
|
+
if self.json_formatter:
|
|
126
|
+
await self.json_formatter.log_request(
|
|
127
|
+
request_id=request_id,
|
|
128
|
+
method=method,
|
|
129
|
+
url=url,
|
|
130
|
+
headers=headers_any,
|
|
131
|
+
body=body, # Pass original body data directly
|
|
132
|
+
request_type="provider" if is_provider_request else "http",
|
|
133
|
+
hook_type="core_http", # Indicate this came from core HTTPTracerHook
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Log with raw HTTP formatter
|
|
137
|
+
if self.raw_formatter:
|
|
138
|
+
# Build raw HTTP request
|
|
139
|
+
raw_request = self._build_raw_http_request(
|
|
140
|
+
method, url, headers_pairs, body, is_json
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Use appropriate logging method based on request type
|
|
144
|
+
if is_provider_request:
|
|
145
|
+
await self.raw_formatter.log_provider_request(
|
|
146
|
+
request_id=request_id,
|
|
147
|
+
raw_data=raw_request,
|
|
148
|
+
hook_type="core_http", # Indicate this came from core HTTPTracerHook
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
await self.raw_formatter.log_client_request(
|
|
152
|
+
request_id=request_id,
|
|
153
|
+
raw_data=raw_request,
|
|
154
|
+
hook_type="core_http", # Indicate this came from core HTTPTracerHook
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def _log_http_response(self, context: HookContext) -> None:
|
|
158
|
+
"""Log an HTTP response.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
context: Hook context with response data
|
|
162
|
+
"""
|
|
163
|
+
request_id = context.data.get("request_id", str(uuid.uuid4()))
|
|
164
|
+
status_code = context.data.get("status_code", 0)
|
|
165
|
+
headers_any = context.data.get("response_headers", {})
|
|
166
|
+
headers_pairs = self._normalize_header_pairs(headers_any)
|
|
167
|
+
body_any = context.data.get("response_body")
|
|
168
|
+
url = context.data.get("url", "")
|
|
169
|
+
|
|
170
|
+
# Determine if this is a provider response
|
|
171
|
+
# First check explicit context markers, then fall back to URL analysis
|
|
172
|
+
if context.data.get("is_provider_response"):
|
|
173
|
+
is_provider_response = True
|
|
174
|
+
elif context.data.get("is_client_response"):
|
|
175
|
+
is_provider_response = False
|
|
176
|
+
else:
|
|
177
|
+
# Fall back to URL analysis for backward compatibility
|
|
178
|
+
is_provider_response = self._is_provider_request(url)
|
|
179
|
+
|
|
180
|
+
logger.debug(
|
|
181
|
+
"core_http_response",
|
|
182
|
+
request_id=request_id,
|
|
183
|
+
status_code=status_code,
|
|
184
|
+
is_provider_response=is_provider_response,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Log with JSON formatter
|
|
188
|
+
if self.json_formatter:
|
|
189
|
+
# Normalize body to bytes for formatter typing
|
|
190
|
+
if body_any is None:
|
|
191
|
+
body_bytes = b""
|
|
192
|
+
elif isinstance(body_any, bytes):
|
|
193
|
+
body_bytes = body_any
|
|
194
|
+
elif isinstance(body_any, str):
|
|
195
|
+
body_bytes = body_any.encode("utf-8")
|
|
196
|
+
else:
|
|
197
|
+
body_bytes = json.dumps(body_any).encode("utf-8")
|
|
198
|
+
|
|
199
|
+
await self.json_formatter.log_response(
|
|
200
|
+
request_id=request_id,
|
|
201
|
+
status=status_code,
|
|
202
|
+
headers=headers_any,
|
|
203
|
+
body=body_bytes,
|
|
204
|
+
response_type="provider" if is_provider_response else "http",
|
|
205
|
+
hook_type="core_http", # Indicate this came from core HTTPTracerHook
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Log with raw HTTP formatter
|
|
209
|
+
if self.raw_formatter:
|
|
210
|
+
# Build raw HTTP response
|
|
211
|
+
raw_response = self._build_raw_http_response(
|
|
212
|
+
status_code, headers_pairs, body_any
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Use appropriate logging method based on response type
|
|
217
|
+
if is_provider_response:
|
|
218
|
+
await self.raw_formatter.log_provider_response(
|
|
219
|
+
request_id=request_id,
|
|
220
|
+
raw_data=raw_response,
|
|
221
|
+
hook_type="core_http", # Indicate this came from core HTTPTracerHook
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
await self.raw_formatter.log_client_response(
|
|
225
|
+
request_id=request_id,
|
|
226
|
+
raw_data=raw_response,
|
|
227
|
+
hook_type="core_http", # Indicate this came from core HTTPTracerHook
|
|
228
|
+
)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(
|
|
231
|
+
"core_http_tracer_hook_response_logging_error",
|
|
232
|
+
request_id=request_id,
|
|
233
|
+
error=str(e),
|
|
234
|
+
exc_info=e,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
async def _log_http_error(self, context: HookContext) -> None:
|
|
238
|
+
"""Log an HTTP error.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
context: Hook context with error data
|
|
242
|
+
"""
|
|
243
|
+
request_id = context.data.get("request_id", str(uuid.uuid4()))
|
|
244
|
+
error_type = context.data.get("error_type", "unknown")
|
|
245
|
+
error_detail = context.data.get("error_detail", "")
|
|
246
|
+
status_code = context.data.get("status_code", 0)
|
|
247
|
+
response_body = context.data.get("response_body", "")
|
|
248
|
+
url = context.data.get("url", "")
|
|
249
|
+
|
|
250
|
+
# Determine if this is a provider error
|
|
251
|
+
is_provider_error = self._is_provider_request(url)
|
|
252
|
+
|
|
253
|
+
logger.error(
|
|
254
|
+
"core_http_error",
|
|
255
|
+
request_id=request_id,
|
|
256
|
+
error_type=error_type,
|
|
257
|
+
status_code=status_code,
|
|
258
|
+
error_detail=error_detail,
|
|
259
|
+
is_provider_error=is_provider_error,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Log error response with formatters
|
|
263
|
+
if self.json_formatter:
|
|
264
|
+
await self.json_formatter.log_error(
|
|
265
|
+
request_id=request_id,
|
|
266
|
+
error=Exception(f"{error_type}: {error_detail}"),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if self.raw_formatter and status_code > 0:
|
|
270
|
+
# Build error response
|
|
271
|
+
raw_response = f"HTTP/1.1 {status_code} Error\r\n\r\n{response_body}"
|
|
272
|
+
|
|
273
|
+
# Use appropriate logging method based on error type
|
|
274
|
+
if is_provider_error:
|
|
275
|
+
await self.raw_formatter.log_provider_response(
|
|
276
|
+
request_id=request_id,
|
|
277
|
+
raw_data=raw_response.encode(),
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
await self.raw_formatter.log_client_response(
|
|
281
|
+
request_id=request_id,
|
|
282
|
+
raw_data=raw_response.encode(),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _build_raw_http_request(
|
|
286
|
+
self,
|
|
287
|
+
method: str,
|
|
288
|
+
url: str,
|
|
289
|
+
headers_pairs: list[tuple[str, str]] | Any,
|
|
290
|
+
body: Any,
|
|
291
|
+
is_json: bool,
|
|
292
|
+
) -> bytes:
|
|
293
|
+
"""Build raw HTTP request for logging.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
method: HTTP method
|
|
297
|
+
url: Request URL
|
|
298
|
+
headers: Request headers
|
|
299
|
+
body: Request body
|
|
300
|
+
is_json: Whether body is JSON
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Raw HTTP request bytes
|
|
304
|
+
"""
|
|
305
|
+
# Parse URL to get path
|
|
306
|
+
from urllib.parse import urlparse
|
|
307
|
+
|
|
308
|
+
parsed = urlparse(url)
|
|
309
|
+
path = parsed.path or "/"
|
|
310
|
+
if parsed.query:
|
|
311
|
+
path += f"?{parsed.query}"
|
|
312
|
+
|
|
313
|
+
# Build request line
|
|
314
|
+
lines = [f"{method} {path} HTTP/1.1"]
|
|
315
|
+
|
|
316
|
+
headers_list = self._normalize_header_pairs(headers_pairs)
|
|
317
|
+
# Add Host header only if not already present in headers
|
|
318
|
+
has_host = any(k.lower() == "host" for k, _ in headers_list)
|
|
319
|
+
if parsed.netloc and not has_host:
|
|
320
|
+
lines.append(f"Host: {parsed.netloc}")
|
|
321
|
+
|
|
322
|
+
# Add other headers (preserve input order, duplicates allowed)
|
|
323
|
+
for key, value in headers_list:
|
|
324
|
+
lines.append(f"{key}: {value}")
|
|
325
|
+
|
|
326
|
+
# Add body
|
|
327
|
+
body_str = ""
|
|
328
|
+
if body:
|
|
329
|
+
if is_json and isinstance(body, dict):
|
|
330
|
+
body_str = json.dumps(body)
|
|
331
|
+
elif isinstance(body, bytes):
|
|
332
|
+
try:
|
|
333
|
+
body_str = body.decode()
|
|
334
|
+
except (UnicodeDecodeError, AttributeError):
|
|
335
|
+
body_str = str(body)
|
|
336
|
+
else:
|
|
337
|
+
body_str = str(body)
|
|
338
|
+
|
|
339
|
+
# Add Content-Length only if not already present in headers
|
|
340
|
+
has_cl = any(k.lower() == "content-length" for k, _ in headers_list)
|
|
341
|
+
if not has_cl:
|
|
342
|
+
lines.append(f"Content-Length: {len(body_str)}")
|
|
343
|
+
lines.append("")
|
|
344
|
+
lines.append(body_str)
|
|
345
|
+
else:
|
|
346
|
+
lines.append("")
|
|
347
|
+
|
|
348
|
+
return "\r\n".join(lines).encode()
|
|
349
|
+
|
|
350
|
+
def _build_raw_http_response(
|
|
351
|
+
self,
|
|
352
|
+
status_code: int,
|
|
353
|
+
headers_pairs: list[tuple[str, str]] | Any,
|
|
354
|
+
body: Any,
|
|
355
|
+
) -> bytes:
|
|
356
|
+
"""Build raw HTTP response for logging.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
status_code: HTTP status code
|
|
360
|
+
headers: Response headers
|
|
361
|
+
body: Response body
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Raw HTTP response bytes
|
|
365
|
+
"""
|
|
366
|
+
# Build status line
|
|
367
|
+
lines = [f"HTTP/1.1 {status_code} OK"]
|
|
368
|
+
|
|
369
|
+
# Add headers (preserve order and duplicates)
|
|
370
|
+
headers_list = self._normalize_header_pairs(headers_pairs)
|
|
371
|
+
for key, value in headers_list:
|
|
372
|
+
lines.append(f"{key}: {value}")
|
|
373
|
+
|
|
374
|
+
# Add body
|
|
375
|
+
if body:
|
|
376
|
+
if isinstance(body, bytes):
|
|
377
|
+
try:
|
|
378
|
+
body_str = body.decode("utf-8")
|
|
379
|
+
except UnicodeDecodeError:
|
|
380
|
+
body_str = body.decode("utf-8", errors="replace")
|
|
381
|
+
elif isinstance(body, dict):
|
|
382
|
+
body_str = json.dumps(body, indent=2)
|
|
383
|
+
else:
|
|
384
|
+
body_str = str(body)
|
|
385
|
+
|
|
386
|
+
# Add Content-Length only if not already present in headers
|
|
387
|
+
has_cl = any(k.lower() == "content-length" for k, _ in headers_list)
|
|
388
|
+
if not has_cl:
|
|
389
|
+
lines.append(f"Content-Length: {len(body_str)}")
|
|
390
|
+
lines.append("")
|
|
391
|
+
lines.append(body_str)
|
|
392
|
+
else:
|
|
393
|
+
lines.append("")
|
|
394
|
+
|
|
395
|
+
return "\r\n".join(lines).encode()
|
|
396
|
+
|
|
397
|
+
def _is_provider_request(self, url: str) -> bool:
|
|
398
|
+
"""Determine if this is a request to a provider API.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
url: The request URL
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if this is a provider request, False for client requests
|
|
405
|
+
"""
|
|
406
|
+
# Known provider domains
|
|
407
|
+
provider_domains = [
|
|
408
|
+
"api.anthropic.com",
|
|
409
|
+
"claude.ai",
|
|
410
|
+
"api.openai.com",
|
|
411
|
+
"chatgpt.com",
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
# Check if URL contains any provider domain
|
|
415
|
+
url_lower = url.lower()
|
|
416
|
+
return any(domain in url_lower for domain in provider_domains)
|
|
417
|
+
|
|
418
|
+
def _normalize_header_pairs(self, headers: Any) -> list[tuple[str, str]]:
|
|
419
|
+
"""Normalize headers to a list of pairs preserving order and duplicates.
|
|
420
|
+
|
|
421
|
+
Accepts dict (items()), dict-like objects, or any iterable of pairs.
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
if headers is None:
|
|
425
|
+
return []
|
|
426
|
+
if hasattr(headers, "items") and callable(headers.items):
|
|
427
|
+
return [(str(k), str(v)) for k, v in headers.items()]
|
|
428
|
+
# Already a sequence of pairs
|
|
429
|
+
return [(str(k), str(v)) for k, v in headers]
|
|
430
|
+
except Exception:
|
|
431
|
+
return []
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Standard hook execution layers for priority ordering."""
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HookLayer(IntEnum):
|
|
7
|
+
"""Standard hook execution priority layers.
|
|
8
|
+
|
|
9
|
+
Hooks execute in priority order from lowest to highest value.
|
|
10
|
+
Within the same priority, hooks execute in registration order.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Pre-processing: Core system setup
|
|
14
|
+
CRITICAL = 0 # System-critical hooks (request ID generation, core context)
|
|
15
|
+
VALIDATION = 100 # Input validation and sanitization
|
|
16
|
+
|
|
17
|
+
# Context building: Authentication and enrichment
|
|
18
|
+
AUTH = 200 # Authentication and authorization
|
|
19
|
+
ENRICHMENT = 300 # Context enrichment (session data, user info, metadata)
|
|
20
|
+
|
|
21
|
+
# Core processing: Business logic
|
|
22
|
+
PROCESSING = 500 # Main request/response processing
|
|
23
|
+
|
|
24
|
+
# Observation: Metrics and logging
|
|
25
|
+
OBSERVATION = 700 # Metrics collection, access logging, tracing
|
|
26
|
+
|
|
27
|
+
# Post-processing: Cleanup and finalization
|
|
28
|
+
CLEANUP = 900 # Resource cleanup, connection management
|
|
29
|
+
FINALIZATION = 1000 # Final operations before response
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Convenience aliases for common use cases
|
|
33
|
+
BEFORE_AUTH = HookLayer.AUTH - 10
|
|
34
|
+
AFTER_AUTH = HookLayer.AUTH + 10
|
|
35
|
+
|
|
36
|
+
BEFORE_PROCESSING = HookLayer.PROCESSING - 10
|
|
37
|
+
AFTER_PROCESSING = HookLayer.PROCESSING + 10
|
|
38
|
+
|
|
39
|
+
# Observation layer ordering (metrics first, logging last)
|
|
40
|
+
METRICS = HookLayer.OBSERVATION # 700: Collect metrics
|
|
41
|
+
TRACING = HookLayer.OBSERVATION + 20 # 720: Request tracing
|
|
42
|
+
ACCESS_LOGGING = (
|
|
43
|
+
HookLayer.OBSERVATION + 50
|
|
44
|
+
) # 750: Access logs (last to capture all data)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Hook execution manager for CCProxy.
|
|
2
|
+
|
|
3
|
+
This module provides the HookManager class which handles the execution of hooks
|
|
4
|
+
for various events in the system. It ensures proper error isolation and supports
|
|
5
|
+
both async and sync hooks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from .base import Hook, HookContext
|
|
15
|
+
from .events import HookEvent
|
|
16
|
+
from .registry import HookRegistry
|
|
17
|
+
from .thread_manager import BackgroundHookThreadManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HookManager:
|
|
21
|
+
"""Manages hook execution with error isolation and async/sync support.
|
|
22
|
+
|
|
23
|
+
The HookManager is responsible for emitting events to registered hooks
|
|
24
|
+
and ensuring that hook failures don't crash the system. It handles both
|
|
25
|
+
async and sync hooks by running sync hooks in a thread pool.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
registry: HookRegistry,
|
|
31
|
+
background_manager: BackgroundHookThreadManager | None = None,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize the hook manager.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
registry: The hook registry to get hooks from
|
|
37
|
+
background_manager: Optional background thread manager for fire-and-forget execution
|
|
38
|
+
"""
|
|
39
|
+
self._registry = registry
|
|
40
|
+
self._background_manager = background_manager
|
|
41
|
+
self._logger = structlog.get_logger(__name__)
|
|
42
|
+
|
|
43
|
+
async def emit(
|
|
44
|
+
self,
|
|
45
|
+
event: HookEvent,
|
|
46
|
+
data: dict[str, Any] | None = None,
|
|
47
|
+
fire_and_forget: bool = True,
|
|
48
|
+
**kwargs: Any,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Emit an event to all registered hooks.
|
|
51
|
+
|
|
52
|
+
Creates a HookContext with the provided data and emits it to all
|
|
53
|
+
hooks registered for the given event. Handles errors gracefully
|
|
54
|
+
to ensure one failing hook doesn't affect others.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
event: The event to emit
|
|
58
|
+
data: Optional data dictionary to include in context
|
|
59
|
+
fire_and_forget: If True, execute hooks in background thread (default)
|
|
60
|
+
**kwargs: Additional context fields (request, response, provider, etc.)
|
|
61
|
+
"""
|
|
62
|
+
context = HookContext(
|
|
63
|
+
event=event,
|
|
64
|
+
timestamp=datetime.utcnow(),
|
|
65
|
+
data=data or {},
|
|
66
|
+
metadata={},
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if fire_and_forget and self._background_manager:
|
|
71
|
+
# Execute in background thread - non-blocking
|
|
72
|
+
self._background_manager.emit_async(context, self._registry)
|
|
73
|
+
return
|
|
74
|
+
elif fire_and_forget and not self._background_manager:
|
|
75
|
+
# No background manager available, log warning and fall back to sync
|
|
76
|
+
self._logger.warning(
|
|
77
|
+
"fire_and_forget_requested_but_no_background_manager_available"
|
|
78
|
+
)
|
|
79
|
+
# Fall through to synchronous execution
|
|
80
|
+
|
|
81
|
+
# Synchronous execution (legacy behavior)
|
|
82
|
+
hooks = self._registry.get(event)
|
|
83
|
+
if not hooks:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Log execution order if debug logging enabled
|
|
87
|
+
self._logger.debug(
|
|
88
|
+
"hook_execution_order",
|
|
89
|
+
hook_event=event.value if hasattr(event, "value") else str(event),
|
|
90
|
+
hooks=[
|
|
91
|
+
{"name": h.name, "priority": getattr(h, "priority", 500)} for h in hooks
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Execute all hooks in priority order, catching errors
|
|
96
|
+
for hook in hooks:
|
|
97
|
+
try:
|
|
98
|
+
await self._execute_hook(hook, context)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self._logger.error(
|
|
101
|
+
"hook_execution_failed",
|
|
102
|
+
hook=hook.name,
|
|
103
|
+
hook_event=event.value if hasattr(event, "value") else str(event),
|
|
104
|
+
priority=getattr(hook, "priority", 500),
|
|
105
|
+
error=str(e),
|
|
106
|
+
)
|
|
107
|
+
# Continue executing other hooks
|
|
108
|
+
|
|
109
|
+
async def emit_with_context(
|
|
110
|
+
self, context: HookContext, fire_and_forget: bool = True
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Emit an event using a pre-built HookContext.
|
|
113
|
+
|
|
114
|
+
This is useful when you need to build the context with specific metadata
|
|
115
|
+
before emitting the event.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
context: The HookContext to emit
|
|
119
|
+
fire_and_forget: If True, execute hooks in background thread (default)
|
|
120
|
+
"""
|
|
121
|
+
if fire_and_forget and self._background_manager:
|
|
122
|
+
# Execute in background thread - non-blocking
|
|
123
|
+
self._background_manager.emit_async(context, self._registry)
|
|
124
|
+
return
|
|
125
|
+
elif fire_and_forget and not self._background_manager:
|
|
126
|
+
# No background manager available, log warning and fall back to sync
|
|
127
|
+
self._logger.warning(
|
|
128
|
+
"fire_and_forget_requested_but_no_background_manager_available"
|
|
129
|
+
)
|
|
130
|
+
# Fall through to synchronous execution
|
|
131
|
+
|
|
132
|
+
# Synchronous execution (legacy behavior)
|
|
133
|
+
hooks = self._registry.get(context.event)
|
|
134
|
+
if not hooks:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Log execution order if debug logging enabled
|
|
138
|
+
self._logger.debug(
|
|
139
|
+
"hook_execution_order",
|
|
140
|
+
hook_event=context.event.value
|
|
141
|
+
if hasattr(context.event, "value")
|
|
142
|
+
else str(context.event),
|
|
143
|
+
hooks=[
|
|
144
|
+
{"name": h.name, "priority": getattr(h, "priority", 500)} for h in hooks
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Execute all hooks in priority order, catching errors
|
|
149
|
+
for hook in hooks:
|
|
150
|
+
try:
|
|
151
|
+
await self._execute_hook(hook, context)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
self._logger.error(
|
|
154
|
+
"hook_execution_failed",
|
|
155
|
+
hook=hook.name,
|
|
156
|
+
hook_event=context.event.value
|
|
157
|
+
if hasattr(context.event, "value")
|
|
158
|
+
else str(context.event),
|
|
159
|
+
priority=getattr(hook, "priority", 500),
|
|
160
|
+
error=str(e),
|
|
161
|
+
)
|
|
162
|
+
# Continue executing other hooks
|
|
163
|
+
|
|
164
|
+
async def _execute_hook(self, hook: Hook, context: HookContext) -> None:
|
|
165
|
+
"""Execute a single hook with proper async/sync handling.
|
|
166
|
+
|
|
167
|
+
Determines if the hook is async or sync and executes it appropriately.
|
|
168
|
+
Sync hooks are run in a thread pool to avoid blocking the async event loop.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
hook: The hook to execute
|
|
172
|
+
context: The context to pass to the hook
|
|
173
|
+
"""
|
|
174
|
+
result = hook(context)
|
|
175
|
+
if asyncio.iscoroutine(result):
|
|
176
|
+
await result
|
|
177
|
+
# If result is None, it was a sync hook and we're done
|
|
178
|
+
|
|
179
|
+
def shutdown(self) -> None:
|
|
180
|
+
"""Shutdown the background hook processing.
|
|
181
|
+
|
|
182
|
+
This method should be called during application shutdown to ensure
|
|
183
|
+
proper cleanup of the background thread.
|
|
184
|
+
"""
|
|
185
|
+
if self._background_manager:
|
|
186
|
+
self._background_manager.stop()
|