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,403 @@
|
|
|
1
|
+
"""Hook-based metrics collection implementation."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.core.plugins.hooks import Hook
|
|
7
|
+
from ccproxy.core.plugins.hooks.base import HookContext
|
|
8
|
+
from ccproxy.core.plugins.hooks.events import HookEvent
|
|
9
|
+
|
|
10
|
+
from .collector import PrometheusMetrics
|
|
11
|
+
from .config import MetricsConfig
|
|
12
|
+
from .pushgateway import PushgatewayClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = get_plugin_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MetricsHook(Hook):
|
|
19
|
+
"""Hook-based metrics collection implementation.
|
|
20
|
+
|
|
21
|
+
This hook listens to request/response lifecycle events and updates
|
|
22
|
+
Prometheus metrics accordingly. It provides event-driven metric
|
|
23
|
+
collection without requiring direct metric calls in the code.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name = "metrics"
|
|
27
|
+
events = [
|
|
28
|
+
HookEvent.REQUEST_STARTED,
|
|
29
|
+
HookEvent.REQUEST_COMPLETED,
|
|
30
|
+
HookEvent.REQUEST_FAILED,
|
|
31
|
+
HookEvent.PROVIDER_REQUEST_PREPARED,
|
|
32
|
+
HookEvent.PROVIDER_RESPONSE_RECEIVED,
|
|
33
|
+
HookEvent.PROVIDER_ERROR,
|
|
34
|
+
HookEvent.PROVIDER_STREAM_START,
|
|
35
|
+
HookEvent.PROVIDER_STREAM_CHUNK,
|
|
36
|
+
HookEvent.PROVIDER_STREAM_END,
|
|
37
|
+
]
|
|
38
|
+
priority = 700 # HookLayer.OBSERVATION - Metrics collection first
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: MetricsConfig | None = None) -> None:
|
|
41
|
+
"""Initialize the metrics hook.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Metrics configuration
|
|
45
|
+
"""
|
|
46
|
+
self.config = config or MetricsConfig()
|
|
47
|
+
|
|
48
|
+
# Initialize collectors based on config using an isolated registry to
|
|
49
|
+
# avoid global REGISTRY collisions in multi-app/test environments.
|
|
50
|
+
if self.config.enabled:
|
|
51
|
+
registry = None
|
|
52
|
+
try:
|
|
53
|
+
from prometheus_client import (
|
|
54
|
+
CollectorRegistry as CollectorRegistry,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
registry = CollectorRegistry()
|
|
58
|
+
except Exception:
|
|
59
|
+
registry = None
|
|
60
|
+
|
|
61
|
+
self.collector: PrometheusMetrics | None = PrometheusMetrics(
|
|
62
|
+
namespace=self.config.namespace,
|
|
63
|
+
histogram_buckets=self.config.histogram_buckets,
|
|
64
|
+
registry=registry,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
self.collector = None
|
|
68
|
+
|
|
69
|
+
self.pushgateway: PushgatewayClient | None = (
|
|
70
|
+
PushgatewayClient(self.config)
|
|
71
|
+
if self.config.pushgateway_enabled and self.config.enabled
|
|
72
|
+
else None
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Track active requests and their start times
|
|
76
|
+
self._request_start_times: dict[str, float] = {}
|
|
77
|
+
|
|
78
|
+
logger.debug(
|
|
79
|
+
"metrics_configured",
|
|
80
|
+
enabled=self.config.enabled,
|
|
81
|
+
namespace=self.config.namespace,
|
|
82
|
+
pushgateway_enabled=self.config.pushgateway_enabled,
|
|
83
|
+
pushgateway_url=self.config.pushgateway_url,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def __call__(self, context: HookContext) -> None:
|
|
87
|
+
"""Handle hook events for metrics collection.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
context: Hook context with event data
|
|
91
|
+
"""
|
|
92
|
+
if not self.config.enabled or not self.collector:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Map hook events to handler methods
|
|
96
|
+
handlers = {
|
|
97
|
+
HookEvent.REQUEST_STARTED: self._handle_request_start,
|
|
98
|
+
HookEvent.REQUEST_COMPLETED: self._handle_request_complete,
|
|
99
|
+
HookEvent.REQUEST_FAILED: self._handle_request_failed,
|
|
100
|
+
HookEvent.PROVIDER_REQUEST_PREPARED: self._handle_provider_request,
|
|
101
|
+
HookEvent.PROVIDER_RESPONSE_RECEIVED: self._handle_provider_response,
|
|
102
|
+
HookEvent.PROVIDER_ERROR: self._handle_provider_error,
|
|
103
|
+
HookEvent.PROVIDER_STREAM_START: self._handle_stream_start,
|
|
104
|
+
HookEvent.PROVIDER_STREAM_CHUNK: self._handle_stream_chunk,
|
|
105
|
+
HookEvent.PROVIDER_STREAM_END: self._handle_stream_end,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
handler = handlers.get(context.event)
|
|
109
|
+
if handler:
|
|
110
|
+
try:
|
|
111
|
+
await handler(context)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(
|
|
114
|
+
"metrics_hook_error",
|
|
115
|
+
hook_event=context.event.value if context.event else "unknown",
|
|
116
|
+
error=str(e),
|
|
117
|
+
exc_info=e,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def _handle_request_start(self, context: HookContext) -> None:
|
|
121
|
+
"""Handle REQUEST_STARTED event."""
|
|
122
|
+
if not self.config.collect_request_metrics or not self.collector:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
request_id = context.data.get("request_id", "unknown")
|
|
126
|
+
|
|
127
|
+
# Track request start time
|
|
128
|
+
self._request_start_times[request_id] = time.time()
|
|
129
|
+
|
|
130
|
+
# Increment active requests
|
|
131
|
+
self.collector.inc_active_requests()
|
|
132
|
+
|
|
133
|
+
logger.debug(
|
|
134
|
+
"metrics_request_started",
|
|
135
|
+
request_id=request_id,
|
|
136
|
+
active_requests=len(self._request_start_times),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def _handle_request_complete(self, context: HookContext) -> None:
|
|
140
|
+
"""Handle REQUEST_COMPLETED event."""
|
|
141
|
+
if not self.config.collect_request_metrics or not self.collector:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
request_id = context.data.get("request_id", "unknown")
|
|
145
|
+
method = context.data.get("method", "UNKNOWN")
|
|
146
|
+
endpoint = context.data.get("endpoint", context.data.get("url", "/"))
|
|
147
|
+
model = context.data.get("model")
|
|
148
|
+
status_code = context.data.get(
|
|
149
|
+
"response_status", context.data.get("status_code", 200)
|
|
150
|
+
)
|
|
151
|
+
service_type = context.data.get("service_type", "unknown")
|
|
152
|
+
|
|
153
|
+
# Calculate duration if we have start time
|
|
154
|
+
duration_seconds = 0.0
|
|
155
|
+
if request_id in self._request_start_times:
|
|
156
|
+
start_time = self._request_start_times.pop(request_id)
|
|
157
|
+
duration_seconds = time.time() - start_time
|
|
158
|
+
elif "duration" in context.data:
|
|
159
|
+
# Use provided duration if available
|
|
160
|
+
duration_seconds = context.data["duration"]
|
|
161
|
+
|
|
162
|
+
# Record metrics
|
|
163
|
+
self.collector.record_request(
|
|
164
|
+
method=method,
|
|
165
|
+
endpoint=endpoint,
|
|
166
|
+
model=model,
|
|
167
|
+
status=status_code,
|
|
168
|
+
service_type=service_type,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if duration_seconds > 0:
|
|
172
|
+
self.collector.record_response_time(
|
|
173
|
+
duration_seconds=duration_seconds,
|
|
174
|
+
model=model,
|
|
175
|
+
endpoint=endpoint,
|
|
176
|
+
service_type=service_type,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Decrement active requests
|
|
180
|
+
self.collector.dec_active_requests()
|
|
181
|
+
|
|
182
|
+
# Handle token metrics if present
|
|
183
|
+
if self.config.collect_token_metrics:
|
|
184
|
+
usage = context.data.get("usage", {})
|
|
185
|
+
if usage:
|
|
186
|
+
if input_tokens := usage.get("input_tokens"):
|
|
187
|
+
self.collector.record_tokens(
|
|
188
|
+
token_count=input_tokens,
|
|
189
|
+
token_type="input",
|
|
190
|
+
model=model,
|
|
191
|
+
service_type=service_type,
|
|
192
|
+
)
|
|
193
|
+
if output_tokens := usage.get("output_tokens"):
|
|
194
|
+
self.collector.record_tokens(
|
|
195
|
+
token_count=output_tokens,
|
|
196
|
+
token_type="output",
|
|
197
|
+
model=model,
|
|
198
|
+
service_type=service_type,
|
|
199
|
+
)
|
|
200
|
+
if cache_read := usage.get("cache_read_input_tokens"):
|
|
201
|
+
self.collector.record_tokens(
|
|
202
|
+
token_count=cache_read,
|
|
203
|
+
token_type="cache_read",
|
|
204
|
+
model=model,
|
|
205
|
+
service_type=service_type,
|
|
206
|
+
)
|
|
207
|
+
if cache_write := usage.get("cache_creation_input_tokens"):
|
|
208
|
+
self.collector.record_tokens(
|
|
209
|
+
token_count=cache_write,
|
|
210
|
+
token_type="cache_write",
|
|
211
|
+
model=model,
|
|
212
|
+
service_type=service_type,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Handle cost metrics if present
|
|
216
|
+
if self.config.collect_cost_metrics and (cost := context.data.get("cost_usd")):
|
|
217
|
+
self.collector.record_cost(
|
|
218
|
+
cost_usd=cost,
|
|
219
|
+
model=model,
|
|
220
|
+
cost_type="total",
|
|
221
|
+
service_type=service_type,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
logger.debug(
|
|
225
|
+
"metrics_request_completed",
|
|
226
|
+
request_id=request_id,
|
|
227
|
+
duration_seconds=duration_seconds,
|
|
228
|
+
status_code=status_code,
|
|
229
|
+
model=model,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def _handle_request_failed(self, context: HookContext) -> None:
|
|
233
|
+
"""Handle REQUEST_FAILED event."""
|
|
234
|
+
if not self.config.collect_error_metrics or not self.collector:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
request_id = context.data.get("request_id", "unknown")
|
|
238
|
+
endpoint = context.data.get("endpoint", context.data.get("url", "/"))
|
|
239
|
+
model = context.data.get("model")
|
|
240
|
+
service_type = context.data.get("service_type", "unknown")
|
|
241
|
+
error = context.error
|
|
242
|
+
error_type = type(error).__name__ if error else "unknown"
|
|
243
|
+
|
|
244
|
+
# Record error
|
|
245
|
+
self.collector.record_error(
|
|
246
|
+
error_type=error_type,
|
|
247
|
+
endpoint=endpoint,
|
|
248
|
+
model=model,
|
|
249
|
+
service_type=service_type,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Record as failed request
|
|
253
|
+
self.collector.record_request(
|
|
254
|
+
method=context.data.get("method", "UNKNOWN"),
|
|
255
|
+
endpoint=endpoint,
|
|
256
|
+
model=model,
|
|
257
|
+
status="error",
|
|
258
|
+
service_type=service_type,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Clean up start time and decrement active requests
|
|
262
|
+
self._request_start_times.pop(request_id, None)
|
|
263
|
+
self.collector.dec_active_requests()
|
|
264
|
+
|
|
265
|
+
logger.debug(
|
|
266
|
+
"metrics_request_failed",
|
|
267
|
+
request_id=request_id,
|
|
268
|
+
error_type=error_type,
|
|
269
|
+
endpoint=endpoint,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
async def _handle_provider_request(self, context: HookContext) -> None:
|
|
273
|
+
"""Handle PROVIDER_REQUEST_PREPARED event."""
|
|
274
|
+
if not self.config.collect_request_metrics:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
provider = context.provider or "unknown"
|
|
278
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
279
|
+
|
|
280
|
+
logger.debug(
|
|
281
|
+
"metrics_provider_request",
|
|
282
|
+
request_id=request_id,
|
|
283
|
+
provider=provider,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def _handle_provider_response(self, context: HookContext) -> None:
|
|
287
|
+
"""Handle PROVIDER_RESPONSE_RECEIVED event."""
|
|
288
|
+
if not self.config.collect_request_metrics:
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
provider = context.provider or "unknown"
|
|
292
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
293
|
+
status_code = context.data.get("status_code", 200)
|
|
294
|
+
|
|
295
|
+
logger.debug(
|
|
296
|
+
"metrics_provider_response",
|
|
297
|
+
request_id=request_id,
|
|
298
|
+
provider=provider,
|
|
299
|
+
status_code=status_code,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async def _handle_provider_error(self, context: HookContext) -> None:
|
|
303
|
+
"""Handle PROVIDER_ERROR event."""
|
|
304
|
+
if not self.config.collect_error_metrics or not self.collector:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
provider = context.provider or "unknown"
|
|
308
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
309
|
+
error = context.error
|
|
310
|
+
error_type = type(error).__name__ if error else "unknown"
|
|
311
|
+
|
|
312
|
+
# Record provider error
|
|
313
|
+
self.collector.record_error(
|
|
314
|
+
error_type=f"provider_{error_type}",
|
|
315
|
+
endpoint=context.data.get("endpoint", "/"),
|
|
316
|
+
model=context.data.get("model"),
|
|
317
|
+
service_type=provider,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
logger.debug(
|
|
321
|
+
"metrics_provider_error",
|
|
322
|
+
request_id=request_id,
|
|
323
|
+
provider=provider,
|
|
324
|
+
error_type=error_type,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
async def _handle_stream_start(self, context: HookContext) -> None:
|
|
328
|
+
"""Handle PROVIDER_STREAM_START event."""
|
|
329
|
+
request_id = context.data.get("request_id", "unknown")
|
|
330
|
+
provider = context.provider or "unknown"
|
|
331
|
+
|
|
332
|
+
logger.debug(
|
|
333
|
+
"metrics_stream_started",
|
|
334
|
+
request_id=request_id,
|
|
335
|
+
provider=provider,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
async def _handle_stream_chunk(self, context: HookContext) -> None:
|
|
339
|
+
"""Handle PROVIDER_STREAM_CHUNK event."""
|
|
340
|
+
# We might not want to record metrics for every chunk
|
|
341
|
+
# due to performance considerations
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
async def _handle_stream_end(self, context: HookContext) -> None:
|
|
345
|
+
"""Handle PROVIDER_STREAM_END event."""
|
|
346
|
+
if not self.config.collect_token_metrics or not self.collector:
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
request_id = context.data.get("request_id", "unknown")
|
|
350
|
+
provider = context.provider or "unknown"
|
|
351
|
+
usage_metrics = context.data.get("usage_metrics", {})
|
|
352
|
+
model = context.data.get("model")
|
|
353
|
+
|
|
354
|
+
# Record streaming token metrics
|
|
355
|
+
if usage_metrics:
|
|
356
|
+
if input_tokens := usage_metrics.get("input_tokens"):
|
|
357
|
+
self.collector.record_tokens(
|
|
358
|
+
token_count=input_tokens,
|
|
359
|
+
token_type="input",
|
|
360
|
+
model=model,
|
|
361
|
+
service_type=provider,
|
|
362
|
+
)
|
|
363
|
+
if output_tokens := usage_metrics.get("output_tokens"):
|
|
364
|
+
self.collector.record_tokens(
|
|
365
|
+
token_count=output_tokens,
|
|
366
|
+
token_type="output",
|
|
367
|
+
model=model,
|
|
368
|
+
service_type=provider,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
logger.debug(
|
|
372
|
+
"metrics_stream_ended",
|
|
373
|
+
request_id=request_id,
|
|
374
|
+
provider=provider,
|
|
375
|
+
usage_metrics=usage_metrics,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def get_collector(self) -> PrometheusMetrics | None:
|
|
379
|
+
"""Get the Prometheus metrics collector instance.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
The metrics collector or None if disabled
|
|
383
|
+
"""
|
|
384
|
+
return self.collector
|
|
385
|
+
|
|
386
|
+
def get_pushgateway_client(self) -> PushgatewayClient | None:
|
|
387
|
+
"""Get the Pushgateway client instance.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The pushgateway client or None if disabled
|
|
391
|
+
"""
|
|
392
|
+
return self.pushgateway
|
|
393
|
+
|
|
394
|
+
async def push_metrics(self) -> bool:
|
|
395
|
+
"""Push current metrics to Pushgateway.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
True if push succeeded, False otherwise
|
|
399
|
+
"""
|
|
400
|
+
if not self.pushgateway or not self.collector or not self.collector.registry:
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
return self.pushgateway.push_metrics(self.collector.registry)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Metrics plugin implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.core.plugins import (
|
|
7
|
+
PluginContext,
|
|
8
|
+
PluginManifest,
|
|
9
|
+
SystemPluginFactory,
|
|
10
|
+
SystemPluginRuntime,
|
|
11
|
+
)
|
|
12
|
+
from ccproxy.core.plugins.hooks import HookRegistry
|
|
13
|
+
|
|
14
|
+
from .config import MetricsConfig
|
|
15
|
+
from .hook import MetricsHook
|
|
16
|
+
from .routes import create_metrics_router
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = get_plugin_logger()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MetricsRuntime(SystemPluginRuntime):
|
|
23
|
+
"""Runtime for metrics plugin."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, manifest: PluginManifest):
|
|
26
|
+
"""Initialize runtime."""
|
|
27
|
+
super().__init__(manifest)
|
|
28
|
+
self.config: MetricsConfig | None = None
|
|
29
|
+
self.hook: MetricsHook | None = None
|
|
30
|
+
self.pushgateway_task_name = "metrics_pushgateway"
|
|
31
|
+
|
|
32
|
+
async def _on_initialize(self) -> None:
|
|
33
|
+
"""Initialize the metrics plugin."""
|
|
34
|
+
if not self.context:
|
|
35
|
+
raise RuntimeError("Context not set")
|
|
36
|
+
|
|
37
|
+
# Get configuration
|
|
38
|
+
config = self.context.get("config")
|
|
39
|
+
if not isinstance(config, MetricsConfig):
|
|
40
|
+
logger.debug("metrics_config_missing")
|
|
41
|
+
# Use default config if none provided
|
|
42
|
+
config = MetricsConfig()
|
|
43
|
+
logger.debug("metrics_configured")
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
if self.config.enabled:
|
|
47
|
+
# Create metrics hook
|
|
48
|
+
self.hook = MetricsHook(self.config)
|
|
49
|
+
|
|
50
|
+
# Register hook with registry
|
|
51
|
+
hook_registry = None
|
|
52
|
+
|
|
53
|
+
# Try direct from context first
|
|
54
|
+
hook_registry = self.context.get("hook_registry")
|
|
55
|
+
logger.debug(
|
|
56
|
+
"hook_registry_from_context",
|
|
57
|
+
found=hook_registry is not None,
|
|
58
|
+
context_keys=list(self.context.keys()) if self.context else [],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# If not found, try app state
|
|
62
|
+
if not hook_registry:
|
|
63
|
+
app = self.context.get("app")
|
|
64
|
+
if app and hasattr(app.state, "hook_registry"):
|
|
65
|
+
hook_registry = app.state.hook_registry
|
|
66
|
+
logger.debug("hook_registry_from_app_state", found=True)
|
|
67
|
+
|
|
68
|
+
if hook_registry and isinstance(hook_registry, HookRegistry):
|
|
69
|
+
hook_registry.register(self.hook)
|
|
70
|
+
logger.debug(
|
|
71
|
+
"metrics_hook_registered",
|
|
72
|
+
namespace=self.config.namespace,
|
|
73
|
+
pushgateway_enabled=self.config.pushgateway_enabled,
|
|
74
|
+
metrics_endpoint_enabled=self.config.metrics_endpoint_enabled,
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
logger.warning(
|
|
78
|
+
"hook_registry_not_available",
|
|
79
|
+
message="Metrics plugin will not collect metrics via hooks",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Register metrics endpoint if enabled
|
|
83
|
+
if self.config.metrics_endpoint_enabled and self.hook:
|
|
84
|
+
app = self.context.get("app")
|
|
85
|
+
if app:
|
|
86
|
+
# Create and register metrics router
|
|
87
|
+
metrics_router = create_metrics_router(self.hook.get_collector())
|
|
88
|
+
app.include_router(metrics_router, prefix="")
|
|
89
|
+
logger.info(
|
|
90
|
+
"metrics_ready",
|
|
91
|
+
enabled=True,
|
|
92
|
+
endpoint="/metrics",
|
|
93
|
+
namespace=self.config.namespace,
|
|
94
|
+
pushgateway_enabled=self.config.pushgateway_enabled,
|
|
95
|
+
pushgateway_url=self.config.pushgateway_url,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Register pushgateway task with scheduler if enabled
|
|
99
|
+
if self.config.pushgateway_enabled and self.hook:
|
|
100
|
+
scheduler = self.context.get("scheduler")
|
|
101
|
+
if scheduler:
|
|
102
|
+
try:
|
|
103
|
+
# Register the task type if not already registered
|
|
104
|
+
from .tasks import PushgatewayTask
|
|
105
|
+
|
|
106
|
+
# Use scheduler's registry (DI), avoiding globals
|
|
107
|
+
registry = scheduler.task_registry
|
|
108
|
+
if not registry.has(self.pushgateway_task_name):
|
|
109
|
+
registry.register(
|
|
110
|
+
self.pushgateway_task_name, PushgatewayTask
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Add task instance to scheduler
|
|
114
|
+
await scheduler.add_task(
|
|
115
|
+
task_name=self.pushgateway_task_name,
|
|
116
|
+
task_type=self.pushgateway_task_name,
|
|
117
|
+
interval_seconds=self.config.pushgateway_push_interval,
|
|
118
|
+
enabled=True,
|
|
119
|
+
max_backoff_seconds=300.0, # Default backoff
|
|
120
|
+
metrics_config=self.config,
|
|
121
|
+
metrics_hook=self.hook,
|
|
122
|
+
)
|
|
123
|
+
logger.info(
|
|
124
|
+
"pushgateway_task_registered",
|
|
125
|
+
task_name=self.pushgateway_task_name,
|
|
126
|
+
url=self.config.pushgateway_url,
|
|
127
|
+
job=self.config.pushgateway_job,
|
|
128
|
+
interval=self.config.pushgateway_push_interval,
|
|
129
|
+
)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(
|
|
132
|
+
"pushgateway_task_registration_failed",
|
|
133
|
+
error=str(e),
|
|
134
|
+
exc_info=e,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
logger.warning(
|
|
138
|
+
"scheduler_not_available",
|
|
139
|
+
message="Pushgateway task will not be scheduled",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
logger.debug(
|
|
143
|
+
"metrics_plugin_enabled",
|
|
144
|
+
namespace=self.config.namespace,
|
|
145
|
+
collect_request_metrics=self.config.collect_request_metrics,
|
|
146
|
+
collect_token_metrics=self.config.collect_token_metrics,
|
|
147
|
+
collect_cost_metrics=self.config.collect_cost_metrics,
|
|
148
|
+
collect_error_metrics=self.config.collect_error_metrics,
|
|
149
|
+
collect_pool_metrics=self.config.collect_pool_metrics,
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
logger.debug("metrics_plugin_disabled")
|
|
153
|
+
|
|
154
|
+
async def _on_shutdown(self) -> None:
|
|
155
|
+
"""Cleanup on shutdown."""
|
|
156
|
+
# Remove pushgateway task from scheduler if registered
|
|
157
|
+
if self.config and self.config.pushgateway_enabled:
|
|
158
|
+
scheduler = None
|
|
159
|
+
if self.context:
|
|
160
|
+
scheduler = self.context.get("scheduler")
|
|
161
|
+
|
|
162
|
+
if scheduler:
|
|
163
|
+
try:
|
|
164
|
+
await scheduler.remove_task(self.pushgateway_task_name)
|
|
165
|
+
logger.debug(
|
|
166
|
+
"pushgateway_task_removed", task_name=self.pushgateway_task_name
|
|
167
|
+
)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning(
|
|
170
|
+
"pushgateway_task_removal_failed",
|
|
171
|
+
task_name=self.pushgateway_task_name,
|
|
172
|
+
error=str(e),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Unregister hook from registry
|
|
176
|
+
if self.hook:
|
|
177
|
+
hook_registry = None
|
|
178
|
+
if self.context:
|
|
179
|
+
app = self.context.get("app")
|
|
180
|
+
if app and hasattr(app.state, "hook_registry"):
|
|
181
|
+
hook_registry = app.state.hook_registry
|
|
182
|
+
if not hook_registry:
|
|
183
|
+
hook_registry = self.context.get("hook_registry")
|
|
184
|
+
|
|
185
|
+
if hook_registry and isinstance(hook_registry, HookRegistry):
|
|
186
|
+
hook_registry.unregister(self.hook)
|
|
187
|
+
logger.debug("metrics_hook_unregistered")
|
|
188
|
+
|
|
189
|
+
# Push final metrics if pushgateway is enabled
|
|
190
|
+
if self.config and self.config.pushgateway_enabled and self.hook:
|
|
191
|
+
try:
|
|
192
|
+
await self.hook.push_metrics()
|
|
193
|
+
logger.info("final_metrics_pushed_to_pushgateway")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(
|
|
196
|
+
"final_metrics_push_failed",
|
|
197
|
+
error=str(e),
|
|
198
|
+
exc_info=e,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
202
|
+
"""Get health check details."""
|
|
203
|
+
details = {
|
|
204
|
+
"type": "system",
|
|
205
|
+
"initialized": self.initialized,
|
|
206
|
+
"enabled": self.config.enabled if self.config else False,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if self.config and self.config.enabled:
|
|
210
|
+
collector_enabled = False
|
|
211
|
+
if self.hook:
|
|
212
|
+
col = self.hook.get_collector()
|
|
213
|
+
collector_enabled = bool(col.is_enabled()) if col else False
|
|
214
|
+
|
|
215
|
+
details.update(
|
|
216
|
+
{
|
|
217
|
+
"namespace": self.config.namespace,
|
|
218
|
+
"metrics_endpoint_enabled": self.config.metrics_endpoint_enabled,
|
|
219
|
+
"pushgateway_enabled": self.config.pushgateway_enabled,
|
|
220
|
+
"pushgateway_url": self.config.pushgateway_url,
|
|
221
|
+
"collector_enabled": collector_enabled,
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return details
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class MetricsFactory(SystemPluginFactory):
|
|
229
|
+
"""Factory for metrics plugin."""
|
|
230
|
+
|
|
231
|
+
def __init__(self) -> None:
|
|
232
|
+
"""Initialize factory with manifest."""
|
|
233
|
+
# Create manifest
|
|
234
|
+
manifest = PluginManifest(
|
|
235
|
+
name="metrics",
|
|
236
|
+
version="0.1.0",
|
|
237
|
+
description="Prometheus metrics collection and export plugin",
|
|
238
|
+
is_provider=False,
|
|
239
|
+
config_class=MetricsConfig,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Initialize with manifest
|
|
243
|
+
super().__init__(manifest)
|
|
244
|
+
|
|
245
|
+
def create_runtime(self) -> MetricsRuntime:
|
|
246
|
+
"""Create runtime instance."""
|
|
247
|
+
return MetricsRuntime(self.manifest)
|
|
248
|
+
|
|
249
|
+
def create_context(self, core_services: Any) -> PluginContext:
|
|
250
|
+
"""Create context for the plugin.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
core_services: Core services from the application
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Plugin context with required services
|
|
257
|
+
"""
|
|
258
|
+
# Get base context
|
|
259
|
+
context = super().create_context(core_services)
|
|
260
|
+
|
|
261
|
+
# The metrics plugin doesn't need special context setup
|
|
262
|
+
# It will get hook_registry and app from the base context
|
|
263
|
+
|
|
264
|
+
return context
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# Export the factory instance
|
|
268
|
+
factory = MetricsFactory()
|