ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Hook-based request tracer implementation for REQUEST_* events only."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import aiofiles
|
|
9
|
+
|
|
10
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
|
+
from ccproxy.core.plugins.hooks import Hook
|
|
12
|
+
from ccproxy.core.plugins.hooks.base import HookContext
|
|
13
|
+
from ccproxy.core.plugins.hooks.events import HookEvent
|
|
14
|
+
|
|
15
|
+
from .config import RequestTracerConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_plugin_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RequestTracerHook(Hook):
|
|
22
|
+
"""Simplified hook-based request tracer implementation.
|
|
23
|
+
|
|
24
|
+
This hook only handles REQUEST_* events since HTTP_* events are now
|
|
25
|
+
handled by the core HTTPTracerHook. This eliminates duplication and
|
|
26
|
+
follows the single responsibility principle.
|
|
27
|
+
|
|
28
|
+
The plugin now focuses purely on request lifecycle logging without
|
|
29
|
+
attempting to capture HTTP request/response bodies.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
name = "request_tracer"
|
|
33
|
+
events = [
|
|
34
|
+
HookEvent.REQUEST_STARTED,
|
|
35
|
+
HookEvent.REQUEST_COMPLETED,
|
|
36
|
+
HookEvent.REQUEST_FAILED,
|
|
37
|
+
HookEvent.PROVIDER_REQUEST_PREPARED,
|
|
38
|
+
HookEvent.PROVIDER_RESPONSE_RECEIVED,
|
|
39
|
+
HookEvent.PROVIDER_ERROR,
|
|
40
|
+
HookEvent.PROVIDER_STREAM_START,
|
|
41
|
+
HookEvent.PROVIDER_STREAM_CHUNK,
|
|
42
|
+
HookEvent.PROVIDER_STREAM_END,
|
|
43
|
+
]
|
|
44
|
+
priority = 300 # HookLayer.ENRICHMENT - Capture/enrich request context early
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
config: RequestTracerConfig | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize the request tracer hook.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
config: Request tracer configuration
|
|
54
|
+
"""
|
|
55
|
+
self.config = config or RequestTracerConfig()
|
|
56
|
+
|
|
57
|
+
# Storage for streaming chunks per request
|
|
58
|
+
self._streaming_chunks: dict[str, list[bytes]] = {}
|
|
59
|
+
self._streaming_metadata: dict[str, dict[str, Any]] = {}
|
|
60
|
+
|
|
61
|
+
logger.debug(
|
|
62
|
+
"request_tracer_hook_initialized",
|
|
63
|
+
enabled=self.config.enabled,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def __call__(self, context: HookContext) -> None:
|
|
67
|
+
"""Handle hook events for request tracing.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
context: Hook context with event data
|
|
71
|
+
"""
|
|
72
|
+
# Debug logging for CLI hook calls
|
|
73
|
+
# logger.trace(
|
|
74
|
+
# "request_tracer_hook_called",
|
|
75
|
+
# hook_event=context.event.value if context.event else "unknown",
|
|
76
|
+
# enabled=self.config.enabled,
|
|
77
|
+
# data_keys=list(context.data.keys()) if context.data else [],
|
|
78
|
+
# )
|
|
79
|
+
|
|
80
|
+
if not self.config.enabled:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Map hook events to handler methods
|
|
84
|
+
handlers = {
|
|
85
|
+
HookEvent.REQUEST_STARTED: self._handle_request_start,
|
|
86
|
+
HookEvent.REQUEST_COMPLETED: self._handle_request_complete,
|
|
87
|
+
HookEvent.REQUEST_FAILED: self._handle_request_failed,
|
|
88
|
+
HookEvent.PROVIDER_REQUEST_PREPARED: self._handle_provider_request,
|
|
89
|
+
HookEvent.PROVIDER_RESPONSE_RECEIVED: self._handle_provider_response,
|
|
90
|
+
HookEvent.PROVIDER_ERROR: self._handle_provider_error,
|
|
91
|
+
HookEvent.PROVIDER_STREAM_START: self._handle_stream_start,
|
|
92
|
+
HookEvent.PROVIDER_STREAM_CHUNK: self._handle_stream_chunk,
|
|
93
|
+
HookEvent.PROVIDER_STREAM_END: self._handle_stream_end,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
handler = handlers.get(context.event)
|
|
97
|
+
if handler:
|
|
98
|
+
try:
|
|
99
|
+
await handler(context)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(
|
|
102
|
+
"request_tracer_hook_error",
|
|
103
|
+
hook_event=context.event.value if context.event else "unknown",
|
|
104
|
+
error=str(e),
|
|
105
|
+
exc_info=e,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def _handle_request_start(self, context: HookContext) -> None:
|
|
109
|
+
"""Handle REQUEST_STARTED event."""
|
|
110
|
+
if not self.config.log_client_request:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Extract request data from context
|
|
114
|
+
request_id = context.data.get("request_id", "unknown")
|
|
115
|
+
method = context.data.get("method", "UNKNOWN")
|
|
116
|
+
url = context.data.get("url", "")
|
|
117
|
+
path = context.data.get("path", url) # Use direct path if available
|
|
118
|
+
|
|
119
|
+
# Check path filters
|
|
120
|
+
if self._should_exclude_path(path):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
logger.trace(
|
|
124
|
+
"request_started",
|
|
125
|
+
request_id=request_id,
|
|
126
|
+
method=method,
|
|
127
|
+
url=url,
|
|
128
|
+
note="Request body logged by core HTTPTracerHook",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def _handle_request_complete(self, context: HookContext) -> None:
|
|
132
|
+
"""Handle REQUEST_COMPLETED event."""
|
|
133
|
+
if not self.config.log_client_response:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
request_id = context.data.get("request_id", "unknown")
|
|
137
|
+
status_code = context.data.get("status_code", 200)
|
|
138
|
+
duration_ms = context.data.get("duration_ms", 0)
|
|
139
|
+
|
|
140
|
+
# Check path filters
|
|
141
|
+
url = context.data.get("url", "")
|
|
142
|
+
path = self._extract_path(url)
|
|
143
|
+
if self._should_exclude_path(path):
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
logger.trace(
|
|
147
|
+
"request_completed",
|
|
148
|
+
request_id=request_id,
|
|
149
|
+
status_code=status_code,
|
|
150
|
+
duration_ms=duration_ms,
|
|
151
|
+
note="Response body logged by core HTTPTracerHook",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def _handle_request_failed(self, context: HookContext) -> None:
|
|
155
|
+
"""Handle REQUEST_FAILED event."""
|
|
156
|
+
request_id = context.data.get("request_id", "unknown")
|
|
157
|
+
error = context.error
|
|
158
|
+
duration = context.data.get("duration", 0)
|
|
159
|
+
|
|
160
|
+
logger.trace(
|
|
161
|
+
"request_failed",
|
|
162
|
+
request_id=request_id,
|
|
163
|
+
error=str(error) if error else "unknown",
|
|
164
|
+
duration=duration,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def _handle_provider_request(self, context: HookContext) -> None:
|
|
168
|
+
"""Handle PROVIDER_REQUEST_PREPARED event."""
|
|
169
|
+
if not self.config.log_provider_request:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
173
|
+
url = context.data.get("url", "")
|
|
174
|
+
method = context.data.get("method", "UNKNOWN")
|
|
175
|
+
provider = context.provider or "unknown"
|
|
176
|
+
|
|
177
|
+
logger.trace(
|
|
178
|
+
"provider_request_prepared",
|
|
179
|
+
request_id=request_id,
|
|
180
|
+
provider=provider,
|
|
181
|
+
method=method,
|
|
182
|
+
url=url,
|
|
183
|
+
note="Request body logged by core HTTPTracerHook",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def _handle_provider_response(self, context: HookContext) -> None:
|
|
187
|
+
"""Handle PROVIDER_RESPONSE_RECEIVED event."""
|
|
188
|
+
if not self.config.log_provider_response:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
192
|
+
status_code = context.data.get("status_code", 200)
|
|
193
|
+
provider = context.provider or "unknown"
|
|
194
|
+
is_streaming = context.data.get("is_streaming", False)
|
|
195
|
+
|
|
196
|
+
logger.trace(
|
|
197
|
+
"provider_response_received",
|
|
198
|
+
request_id=request_id,
|
|
199
|
+
provider=provider,
|
|
200
|
+
status_code=status_code,
|
|
201
|
+
is_streaming=is_streaming,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def _handle_provider_error(self, context: HookContext) -> None:
|
|
205
|
+
"""Handle PROVIDER_ERROR event."""
|
|
206
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
207
|
+
provider = context.provider or "unknown"
|
|
208
|
+
error = context.error
|
|
209
|
+
|
|
210
|
+
logger.error(
|
|
211
|
+
"provider_error",
|
|
212
|
+
request_id=request_id,
|
|
213
|
+
provider=provider,
|
|
214
|
+
error=str(error) if error else "unknown",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
async def _handle_stream_start(self, context: HookContext) -> None:
|
|
218
|
+
"""Handle PROVIDER_STREAM_START event."""
|
|
219
|
+
request_id = context.data.get("request_id") or context.metadata.get(
|
|
220
|
+
"request_id", "unknown"
|
|
221
|
+
)
|
|
222
|
+
provider = context.provider or "unknown"
|
|
223
|
+
|
|
224
|
+
logger.debug(
|
|
225
|
+
"stream_start_handler_called",
|
|
226
|
+
chunk_logging_enabled=self.config.log_streaming_chunks,
|
|
227
|
+
json_logs_enabled=self.config.json_logs_enabled,
|
|
228
|
+
request_id=request_id,
|
|
229
|
+
provider=provider,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if self.config.json_logs_enabled:
|
|
233
|
+
# Initialize chunk collection for this request when JSON logs are enabled
|
|
234
|
+
self._streaming_chunks[request_id] = []
|
|
235
|
+
self._streaming_metadata[request_id] = {
|
|
236
|
+
"provider": provider,
|
|
237
|
+
"start_time": datetime.now(),
|
|
238
|
+
"url": context.data.get("url", ""),
|
|
239
|
+
"method": context.data.get("method", "UNKNOWN"),
|
|
240
|
+
"buffered_mode": context.data.get("buffered_mode", False),
|
|
241
|
+
"upstream_stream_text": None,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logger.debug(
|
|
245
|
+
"stream_started",
|
|
246
|
+
request_id=request_id,
|
|
247
|
+
provider=provider,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def _handle_stream_chunk(self, context: HookContext) -> None:
|
|
251
|
+
"""Handle PROVIDER_STREAM_CHUNK event."""
|
|
252
|
+
request_id = context.data.get("request_id", "unknown")
|
|
253
|
+
chunk = context.data.get("chunk")
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
chunk
|
|
257
|
+
and self.config.json_logs_enabled
|
|
258
|
+
and request_id in self._streaming_chunks
|
|
259
|
+
):
|
|
260
|
+
# Collect the chunk
|
|
261
|
+
self._streaming_chunks[request_id].append(chunk)
|
|
262
|
+
|
|
263
|
+
if not self.config.log_streaming_chunks:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Optional: Log chunk info for debugging
|
|
267
|
+
chunk_number = context.data.get("chunk_number", 0)
|
|
268
|
+
chunk_size = context.data.get("chunk_size", len(chunk) if chunk else 0)
|
|
269
|
+
|
|
270
|
+
logger.trace(
|
|
271
|
+
"stream_chunk_collected",
|
|
272
|
+
request_id=request_id,
|
|
273
|
+
chunk_number=chunk_number,
|
|
274
|
+
chunk_size=chunk_size,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
async def _handle_stream_end(self, context: HookContext) -> None:
|
|
278
|
+
"""Handle PROVIDER_STREAM_END event."""
|
|
279
|
+
request_id = context.data.get("request_id", "unknown")
|
|
280
|
+
provider = context.provider or "unknown"
|
|
281
|
+
total_chunks = context.data.get("total_chunks", 0)
|
|
282
|
+
total_bytes = context.data.get("total_bytes", 0)
|
|
283
|
+
usage_metrics = context.data.get("usage_metrics", {})
|
|
284
|
+
|
|
285
|
+
# Write collected chunks to response file
|
|
286
|
+
if self.config.json_logs_enabled:
|
|
287
|
+
if (
|
|
288
|
+
request_id in self._streaming_chunks
|
|
289
|
+
and self._streaming_chunks[request_id]
|
|
290
|
+
):
|
|
291
|
+
metadata = self._streaming_metadata.get(request_id, {})
|
|
292
|
+
chunks = self._streaming_chunks[request_id]
|
|
293
|
+
|
|
294
|
+
# Add end time and metrics to metadata
|
|
295
|
+
metadata.update(
|
|
296
|
+
{
|
|
297
|
+
"end_time": datetime.now(),
|
|
298
|
+
"total_chunks": total_chunks,
|
|
299
|
+
"total_bytes": total_bytes,
|
|
300
|
+
"usage_metrics": usage_metrics,
|
|
301
|
+
"upstream_stream_text": context.data.get(
|
|
302
|
+
"upstream_stream_text"
|
|
303
|
+
),
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Write response file
|
|
308
|
+
await self._write_streaming_response_file(request_id, chunks, metadata)
|
|
309
|
+
|
|
310
|
+
# Clean up memory regardless of whether we had chunks
|
|
311
|
+
self._streaming_chunks.pop(request_id, None)
|
|
312
|
+
self._streaming_metadata.pop(request_id, None)
|
|
313
|
+
|
|
314
|
+
logger.trace(
|
|
315
|
+
"stream_ended",
|
|
316
|
+
request_id=request_id,
|
|
317
|
+
provider=provider,
|
|
318
|
+
total_chunks=total_chunks,
|
|
319
|
+
total_bytes=total_bytes,
|
|
320
|
+
usage_metrics=usage_metrics,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _extract_path(self, url: str) -> str:
|
|
324
|
+
"""Extract path from URL."""
|
|
325
|
+
if "://" in url:
|
|
326
|
+
# Full URL
|
|
327
|
+
parts = url.split("/", 3)
|
|
328
|
+
return "/" + parts[3] if len(parts) > 3 else "/"
|
|
329
|
+
return url
|
|
330
|
+
|
|
331
|
+
def _should_exclude_path(self, path: str) -> bool:
|
|
332
|
+
"""Check if path should be excluded from logging."""
|
|
333
|
+
# Check include paths first (if specified)
|
|
334
|
+
if self.config.include_paths:
|
|
335
|
+
return not any(path.startswith(p) for p in self.config.include_paths)
|
|
336
|
+
|
|
337
|
+
# Check exclude paths
|
|
338
|
+
if self.config.exclude_paths:
|
|
339
|
+
return any(path.startswith(p) for p in self.config.exclude_paths)
|
|
340
|
+
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
def _generate_stream_response_file_path(
|
|
344
|
+
self, request_id: str, provider: str
|
|
345
|
+
) -> Path:
|
|
346
|
+
"""Generate file path for streaming response file."""
|
|
347
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
348
|
+
filename = f"{request_id}_{timestamp}_{provider}_streaming_response.json"
|
|
349
|
+
return Path(self.config.get_json_log_dir()) / filename
|
|
350
|
+
|
|
351
|
+
async def _write_streaming_response_file(
|
|
352
|
+
self, request_id: str, chunks: list[bytes], metadata: dict[str, Any]
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Write collected streaming chunks to a response file."""
|
|
355
|
+
try:
|
|
356
|
+
# Combine all chunks
|
|
357
|
+
combined_data = b"".join(chunks)
|
|
358
|
+
|
|
359
|
+
# Try to decode as text for JSON parsing
|
|
360
|
+
try:
|
|
361
|
+
response_text = combined_data.decode("utf-8", errors="replace")
|
|
362
|
+
except Exception:
|
|
363
|
+
response_text = str(combined_data)
|
|
364
|
+
|
|
365
|
+
# Build response data
|
|
366
|
+
upstream_stream_text = metadata.get("upstream_stream_text")
|
|
367
|
+
response_data = {
|
|
368
|
+
"request_id": request_id,
|
|
369
|
+
"provider": metadata.get("provider", "unknown"),
|
|
370
|
+
"method": metadata.get("method", "UNKNOWN"),
|
|
371
|
+
"url": metadata.get("url", ""),
|
|
372
|
+
"start_time": metadata.get("start_time", datetime.now()).isoformat(),
|
|
373
|
+
"end_time": datetime.now().isoformat(),
|
|
374
|
+
"total_chunks": len(chunks),
|
|
375
|
+
"total_bytes": len(combined_data),
|
|
376
|
+
"buffered_mode": metadata.get("buffered_mode", False),
|
|
377
|
+
"usage_metrics": metadata.get("usage_metrics"),
|
|
378
|
+
# "response_text": response_text[: self.config.truncate_body_preview]
|
|
379
|
+
# if len(response_text) > self.config.truncate_body_preview
|
|
380
|
+
# else response_text,
|
|
381
|
+
# "response_truncated": len(response_text)
|
|
382
|
+
# > self.config.truncate_body_preview,
|
|
383
|
+
"response_text": response_text,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if upstream_stream_text is not None:
|
|
387
|
+
response_data["upstream_stream_text"] = upstream_stream_text
|
|
388
|
+
|
|
389
|
+
# Generate file path
|
|
390
|
+
file_path = self._generate_stream_response_file_path(
|
|
391
|
+
request_id, metadata.get("provider", "unknown")
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Ensure directory exists
|
|
395
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
396
|
+
|
|
397
|
+
# Write JSON file
|
|
398
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
399
|
+
await f.write(json.dumps(response_data, indent=2, ensure_ascii=False))
|
|
400
|
+
|
|
401
|
+
logger.debug(
|
|
402
|
+
"streaming_response_file_written",
|
|
403
|
+
request_id=request_id,
|
|
404
|
+
file_path=str(file_path),
|
|
405
|
+
total_chunks=len(chunks),
|
|
406
|
+
total_bytes=len(combined_data),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(
|
|
411
|
+
"streaming_response_file_write_failed",
|
|
412
|
+
request_id=request_id,
|
|
413
|
+
error=str(e),
|
|
414
|
+
exc_info=e,
|
|
415
|
+
)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Request Tracer plugin implementation - after refactoring."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
7
|
+
from ccproxy.core.plugins import (
|
|
8
|
+
PluginManifest,
|
|
9
|
+
SystemPluginFactory,
|
|
10
|
+
SystemPluginRuntime,
|
|
11
|
+
)
|
|
12
|
+
from ccproxy.core.plugins.hooks import HookRegistry
|
|
13
|
+
from ccproxy.core.plugins.hooks.implementations import HTTPTracerHook
|
|
14
|
+
from ccproxy.core.plugins.hooks.implementations.formatters import (
|
|
15
|
+
JSONFormatter,
|
|
16
|
+
RawHTTPFormatter,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .config import RequestTracerConfig
|
|
20
|
+
from .hook import RequestTracerHook
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = get_plugin_logger()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RequestTracerRuntime(SystemPluginRuntime):
|
|
27
|
+
"""Runtime for the request tracer plugin.
|
|
28
|
+
|
|
29
|
+
Handles only REQUEST_* events via a hook.
|
|
30
|
+
HTTP events are managed by the core HTTPTracerHook.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, manifest: PluginManifest):
|
|
34
|
+
"""Initialize runtime."""
|
|
35
|
+
super().__init__(manifest)
|
|
36
|
+
self.config: RequestTracerConfig | None = None
|
|
37
|
+
self.hook: RequestTracerHook | None = None
|
|
38
|
+
self.http_tracer_hook: HTTPTracerHook | None = None
|
|
39
|
+
|
|
40
|
+
async def _on_initialize(self) -> None:
|
|
41
|
+
"""Initialize the request tracer."""
|
|
42
|
+
if not self.context:
|
|
43
|
+
raise RuntimeError("Context not set")
|
|
44
|
+
|
|
45
|
+
# Get configuration
|
|
46
|
+
config = self.context.get("config")
|
|
47
|
+
if not isinstance(config, RequestTracerConfig):
|
|
48
|
+
config = RequestTracerConfig()
|
|
49
|
+
|
|
50
|
+
self.config = config
|
|
51
|
+
|
|
52
|
+
# Debug log the actual configuration being used
|
|
53
|
+
logger.debug(
|
|
54
|
+
"plugin_configuration_loaded",
|
|
55
|
+
enabled=config.enabled,
|
|
56
|
+
json_logs_enabled=config.json_logs_enabled,
|
|
57
|
+
verbose_api=config.verbose_api,
|
|
58
|
+
log_dir=config.log_dir,
|
|
59
|
+
exclude_paths=config.exclude_paths,
|
|
60
|
+
log_client_request=config.log_client_request,
|
|
61
|
+
log_client_response=config.log_client_response,
|
|
62
|
+
note="HTTP events handled by core HTTPTracerHook",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Validate configuration
|
|
66
|
+
validation_errors = self._validate_config(config)
|
|
67
|
+
if validation_errors:
|
|
68
|
+
logger.error(
|
|
69
|
+
"plugin_config_validation_failed",
|
|
70
|
+
errors=validation_errors,
|
|
71
|
+
config=config.model_dump()
|
|
72
|
+
if hasattr(config, "model_dump")
|
|
73
|
+
else str(config),
|
|
74
|
+
)
|
|
75
|
+
for error in validation_errors:
|
|
76
|
+
logger.warning("config_validation_warning", issue=error)
|
|
77
|
+
|
|
78
|
+
# Try to get hook registry from context
|
|
79
|
+
hook_registry: HookRegistry | None = self.context.get("hook_registry")
|
|
80
|
+
|
|
81
|
+
# If not found, try app state
|
|
82
|
+
if not hook_registry:
|
|
83
|
+
app = self.context.get("app")
|
|
84
|
+
if app and hasattr(app.state, "hook_registry"):
|
|
85
|
+
hook_registry = app.state.hook_registry
|
|
86
|
+
|
|
87
|
+
if not hook_registry or not isinstance(hook_registry, HookRegistry):
|
|
88
|
+
logger.warning(
|
|
89
|
+
"hook_registry_not_available",
|
|
90
|
+
mode="hooks",
|
|
91
|
+
fallback="disabled",
|
|
92
|
+
)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
settings = self.context.get("settings") if self.context else None
|
|
96
|
+
|
|
97
|
+
json_formatter, raw_formatter = self._build_formatters(settings)
|
|
98
|
+
|
|
99
|
+
self.http_tracer_hook = HTTPTracerHook(
|
|
100
|
+
json_formatter=json_formatter,
|
|
101
|
+
raw_formatter=raw_formatter,
|
|
102
|
+
enabled=True,
|
|
103
|
+
)
|
|
104
|
+
hook_registry.register(self.http_tracer_hook)
|
|
105
|
+
logger.debug(
|
|
106
|
+
"core_http_tracer_registered",
|
|
107
|
+
mode="hooks",
|
|
108
|
+
json_logs_enabled=self.config.json_logs_enabled,
|
|
109
|
+
raw_http_enabled=(raw_formatter is not None),
|
|
110
|
+
log_dir=json_formatter.log_dir if json_formatter else None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Register REQUEST_* hook for contextual logging
|
|
114
|
+
self.hook = RequestTracerHook(self.config)
|
|
115
|
+
hook_registry.register(self.hook)
|
|
116
|
+
logger.debug(
|
|
117
|
+
"request_tracer_hook_registered",
|
|
118
|
+
mode="hooks",
|
|
119
|
+
json_logs=self.config.json_logs_enabled,
|
|
120
|
+
verbose_api=self.config.verbose_api,
|
|
121
|
+
note="HTTP events handled by core HTTPTracerHook",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
logger.debug(
|
|
125
|
+
"request_tracer_enabled",
|
|
126
|
+
log_dir=self.config.log_dir,
|
|
127
|
+
json_logs=self.config.json_logs_enabled,
|
|
128
|
+
exclude_paths=self.config.exclude_paths,
|
|
129
|
+
architecture="hooks_only",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _validate_config(self, config: RequestTracerConfig) -> list[str]:
|
|
133
|
+
"""Validate plugin configuration.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of validation error messages (empty if valid)
|
|
137
|
+
"""
|
|
138
|
+
errors: list[str] = []
|
|
139
|
+
|
|
140
|
+
if not config.enabled:
|
|
141
|
+
return errors # No validation needed if disabled
|
|
142
|
+
|
|
143
|
+
# Basic path validation
|
|
144
|
+
try:
|
|
145
|
+
log_path = Path(config.log_dir)
|
|
146
|
+
if not log_path.parent.exists():
|
|
147
|
+
errors.append(
|
|
148
|
+
f"Parent directory of log_dir does not exist: {log_path.parent}"
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
errors.append(f"Invalid log_dir path: {e}")
|
|
152
|
+
|
|
153
|
+
# Configuration consistency checks
|
|
154
|
+
if not config.json_logs_enabled and not config.verbose_api:
|
|
155
|
+
errors.append(
|
|
156
|
+
"No logging output enabled (json_logs_enabled=False, verbose_api=False)"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if config.max_body_size < 0:
|
|
160
|
+
errors.append("max_body_size cannot be negative")
|
|
161
|
+
|
|
162
|
+
return errors
|
|
163
|
+
|
|
164
|
+
async def _on_shutdown(self) -> None:
|
|
165
|
+
"""Cleanup resources."""
|
|
166
|
+
if self.hook:
|
|
167
|
+
logger.debug("shutting_down_request_tracer_hook")
|
|
168
|
+
self.hook = None
|
|
169
|
+
self.http_tracer_hook = None
|
|
170
|
+
logger.debug("request_tracer_plugin_shutdown_complete")
|
|
171
|
+
|
|
172
|
+
def _build_formatters(
|
|
173
|
+
self,
|
|
174
|
+
settings: Any,
|
|
175
|
+
) -> tuple[JSONFormatter | None, RawHTTPFormatter | None]:
|
|
176
|
+
"""Construct formatters based on plugin and global logging settings."""
|
|
177
|
+
|
|
178
|
+
if not self.config:
|
|
179
|
+
return (None, None)
|
|
180
|
+
|
|
181
|
+
# Determine logging base directory overrides from global settings
|
|
182
|
+
json_log_dir = Path(self.config.get_json_log_dir())
|
|
183
|
+
raw_log_dir = Path(self.config.get_raw_log_dir())
|
|
184
|
+
|
|
185
|
+
override_base: Path | None = None
|
|
186
|
+
if settings and getattr(settings, "logging", None):
|
|
187
|
+
plugin_base = getattr(settings.logging, "plugin_log_base_dir", None)
|
|
188
|
+
if plugin_base:
|
|
189
|
+
override_base = Path(plugin_base) / "tracer"
|
|
190
|
+
|
|
191
|
+
fields_set: set[str] = getattr(self.config, "model_fields_set", set())
|
|
192
|
+
if override_base:
|
|
193
|
+
if "request_log_dir" not in fields_set and "log_dir" not in fields_set:
|
|
194
|
+
json_log_dir = override_base
|
|
195
|
+
if "raw_log_dir" not in fields_set and "log_dir" not in fields_set:
|
|
196
|
+
raw_log_dir = override_base
|
|
197
|
+
|
|
198
|
+
json_formatter: JSONFormatter | None = None
|
|
199
|
+
if self.config.json_logs_enabled:
|
|
200
|
+
json_formatter = JSONFormatter(
|
|
201
|
+
log_dir=str(json_log_dir),
|
|
202
|
+
verbose_api=self.config.verbose_api,
|
|
203
|
+
json_logs_enabled=self.config.json_logs_enabled,
|
|
204
|
+
redact_sensitive=self.config.redact_sensitive,
|
|
205
|
+
truncate_body_preview=self.config.truncate_body_preview,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
raw_formatter: RawHTTPFormatter | None = None
|
|
209
|
+
raw_logging_enabled = self.config.raw_http_enabled
|
|
210
|
+
if raw_logging_enabled:
|
|
211
|
+
raw_formatter = RawHTTPFormatter(
|
|
212
|
+
log_dir=str(raw_log_dir),
|
|
213
|
+
enabled=True,
|
|
214
|
+
log_client_request=self.config.log_client_request,
|
|
215
|
+
log_client_response=self.config.log_client_response,
|
|
216
|
+
log_provider_request=self.config.log_provider_request,
|
|
217
|
+
log_provider_response=self.config.log_provider_response,
|
|
218
|
+
max_body_size=self.config.max_body_size,
|
|
219
|
+
exclude_headers=self.config.exclude_headers,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return (json_formatter, raw_formatter)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class RequestTracerFactory(SystemPluginFactory):
|
|
226
|
+
"""factory for request tracer plugin."""
|
|
227
|
+
|
|
228
|
+
def __init__(self) -> None:
|
|
229
|
+
"""Initialize factory with manifest."""
|
|
230
|
+
# Create manifest with static declarations ( from original)
|
|
231
|
+
manifest = PluginManifest(
|
|
232
|
+
name="request_tracer",
|
|
233
|
+
version="0.1.0", # Standardized initial plugin version
|
|
234
|
+
description=" request tracing for REQUEST_* events only",
|
|
235
|
+
is_provider=False,
|
|
236
|
+
config_class=RequestTracerConfig,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Initialize with manifest
|
|
240
|
+
super().__init__(manifest)
|
|
241
|
+
|
|
242
|
+
def create_runtime(self) -> RequestTracerRuntime:
|
|
243
|
+
"""Create runtime instance."""
|
|
244
|
+
return RequestTracerRuntime(self.manifest)
|
|
245
|
+
|
|
246
|
+
def create_context(self, core_services: Any) -> Any:
|
|
247
|
+
"""Create context for the plugin."""
|
|
248
|
+
# Get base context from parent
|
|
249
|
+
context = super().create_context(core_services)
|
|
250
|
+
|
|
251
|
+
return context
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# Export the factory instance for entry points
|
|
255
|
+
factory = RequestTracerFactory()
|
ccproxy/scheduler/__init__.py
CHANGED
|
@@ -17,24 +17,12 @@ Key components:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
from .core import Scheduler
|
|
20
|
-
from .registry import TaskRegistry
|
|
21
|
-
from .tasks import
|
|
22
|
-
BaseScheduledTask,
|
|
23
|
-
PricingCacheUpdateTask,
|
|
24
|
-
PushgatewayTask,
|
|
25
|
-
StatsPrintingTask,
|
|
26
|
-
)
|
|
20
|
+
from .registry import TaskRegistry
|
|
21
|
+
from .tasks import BaseScheduledTask
|
|
27
22
|
|
|
28
23
|
|
|
29
|
-
# Task registration is now handled in manager.py during scheduler startup
|
|
30
|
-
# to avoid side effects during module imports (e.g., CLI help display)
|
|
31
|
-
|
|
32
24
|
__all__ = [
|
|
33
25
|
"Scheduler",
|
|
34
26
|
"TaskRegistry",
|
|
35
|
-
"register_task",
|
|
36
27
|
"BaseScheduledTask",
|
|
37
|
-
"PushgatewayTask",
|
|
38
|
-
"StatsPrintingTask",
|
|
39
|
-
"PricingCacheUpdateTask",
|
|
40
28
|
]
|