ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,753 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Stats collector and printer for periodic metrics summary.
|
|
3
|
-
|
|
4
|
-
This module provides functionality to collect and print periodic statistics
|
|
5
|
-
from the observability system, including Prometheus metrics and DuckDB storage.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import time
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
import structlog
|
|
17
|
-
|
|
18
|
-
from ccproxy.config.observability import ObservabilitySettings
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
logger = structlog.get_logger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class StatsSnapshot:
|
|
26
|
-
"""Snapshot of current statistics."""
|
|
27
|
-
|
|
28
|
-
timestamp: datetime
|
|
29
|
-
requests_total: int
|
|
30
|
-
requests_last_minute: int
|
|
31
|
-
avg_response_time_ms: float
|
|
32
|
-
avg_response_time_last_minute_ms: float
|
|
33
|
-
tokens_input_total: int
|
|
34
|
-
tokens_output_total: int
|
|
35
|
-
tokens_input_last_minute: int
|
|
36
|
-
tokens_output_last_minute: int
|
|
37
|
-
cost_total_usd: float
|
|
38
|
-
cost_last_minute_usd: float
|
|
39
|
-
errors_total: int
|
|
40
|
-
errors_last_minute: int
|
|
41
|
-
active_requests: int
|
|
42
|
-
top_model: str
|
|
43
|
-
top_model_percentage: float
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class StatsCollector:
|
|
47
|
-
"""
|
|
48
|
-
Collects and formats metrics statistics for periodic printing.
|
|
49
|
-
|
|
50
|
-
Integrates with both Prometheus metrics and DuckDB storage to provide
|
|
51
|
-
comprehensive statistics about the API performance.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
settings: ObservabilitySettings,
|
|
57
|
-
metrics_instance: Any | None = None,
|
|
58
|
-
storage_instance: Any | None = None,
|
|
59
|
-
):
|
|
60
|
-
"""
|
|
61
|
-
Initialize stats collector.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
settings: Observability configuration settings
|
|
65
|
-
metrics_instance: Prometheus metrics instance
|
|
66
|
-
storage_instance: DuckDB storage instance
|
|
67
|
-
"""
|
|
68
|
-
self.settings = settings
|
|
69
|
-
self._metrics_instance = metrics_instance
|
|
70
|
-
self._storage_instance = storage_instance
|
|
71
|
-
self._last_snapshot: StatsSnapshot | None = None
|
|
72
|
-
self._last_collection_time = time.time()
|
|
73
|
-
|
|
74
|
-
async def collect_stats(self) -> StatsSnapshot:
|
|
75
|
-
"""
|
|
76
|
-
Collect current statistics from all available sources.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
StatsSnapshot with current metrics
|
|
80
|
-
"""
|
|
81
|
-
current_time = time.time()
|
|
82
|
-
timestamp = datetime.now()
|
|
83
|
-
|
|
84
|
-
# Initialize default values
|
|
85
|
-
stats_data: dict[str, Any] = {
|
|
86
|
-
"timestamp": timestamp,
|
|
87
|
-
"requests_total": 0,
|
|
88
|
-
"requests_last_minute": 0,
|
|
89
|
-
"avg_response_time_ms": 0.0,
|
|
90
|
-
"avg_response_time_last_minute_ms": 0.0,
|
|
91
|
-
"tokens_input_total": 0,
|
|
92
|
-
"tokens_output_total": 0,
|
|
93
|
-
"tokens_input_last_minute": 0,
|
|
94
|
-
"tokens_output_last_minute": 0,
|
|
95
|
-
"cost_total_usd": 0.0,
|
|
96
|
-
"cost_last_minute_usd": 0.0,
|
|
97
|
-
"errors_total": 0,
|
|
98
|
-
"errors_last_minute": 0,
|
|
99
|
-
"active_requests": 0,
|
|
100
|
-
"top_model": "unknown",
|
|
101
|
-
"top_model_percentage": 0.0,
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
# Collect from Prometheus metrics if available
|
|
105
|
-
if self._metrics_instance and self._metrics_instance.is_enabled():
|
|
106
|
-
try:
|
|
107
|
-
await self._collect_from_prometheus(stats_data)
|
|
108
|
-
except Exception as e:
|
|
109
|
-
logger.warning(
|
|
110
|
-
"Failed to collect from Prometheus metrics", error=str(e)
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Collect from DuckDB storage if available
|
|
114
|
-
if self._storage_instance and self._storage_instance.is_enabled():
|
|
115
|
-
try:
|
|
116
|
-
await self._collect_from_duckdb(stats_data, current_time)
|
|
117
|
-
except Exception as e:
|
|
118
|
-
logger.warning("Failed to collect from DuckDB storage", error=str(e))
|
|
119
|
-
|
|
120
|
-
snapshot = StatsSnapshot(
|
|
121
|
-
timestamp=stats_data["timestamp"],
|
|
122
|
-
requests_total=int(stats_data["requests_total"]),
|
|
123
|
-
requests_last_minute=int(stats_data["requests_last_minute"]),
|
|
124
|
-
avg_response_time_ms=float(stats_data["avg_response_time_ms"]),
|
|
125
|
-
avg_response_time_last_minute_ms=float(
|
|
126
|
-
stats_data["avg_response_time_last_minute_ms"]
|
|
127
|
-
),
|
|
128
|
-
tokens_input_total=int(stats_data["tokens_input_total"]),
|
|
129
|
-
tokens_output_total=int(stats_data["tokens_output_total"]),
|
|
130
|
-
tokens_input_last_minute=int(stats_data["tokens_input_last_minute"]),
|
|
131
|
-
tokens_output_last_minute=int(stats_data["tokens_output_last_minute"]),
|
|
132
|
-
cost_total_usd=float(stats_data["cost_total_usd"]),
|
|
133
|
-
cost_last_minute_usd=float(stats_data["cost_last_minute_usd"]),
|
|
134
|
-
errors_total=int(stats_data["errors_total"]),
|
|
135
|
-
errors_last_minute=int(stats_data["errors_last_minute"]),
|
|
136
|
-
active_requests=int(stats_data["active_requests"]),
|
|
137
|
-
top_model=str(stats_data["top_model"]),
|
|
138
|
-
top_model_percentage=float(stats_data["top_model_percentage"]),
|
|
139
|
-
)
|
|
140
|
-
self._last_snapshot = snapshot
|
|
141
|
-
self._last_collection_time = current_time
|
|
142
|
-
|
|
143
|
-
return snapshot
|
|
144
|
-
|
|
145
|
-
async def _collect_from_prometheus(self, stats_data: dict[str, Any]) -> None:
|
|
146
|
-
"""Collect statistics from Prometheus metrics."""
|
|
147
|
-
if not self._metrics_instance:
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
logger.debug(
|
|
152
|
-
"prometheus_collection_starting",
|
|
153
|
-
metrics_available=bool(self._metrics_instance),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
# Get active requests from gauge
|
|
157
|
-
if hasattr(self._metrics_instance, "active_requests"):
|
|
158
|
-
active_value = self._metrics_instance.active_requests._value._value
|
|
159
|
-
stats_data["active_requests"] = int(active_value)
|
|
160
|
-
logger.debug(
|
|
161
|
-
"prometheus_active_requests_collected", active_requests=active_value
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Get request counts from counter
|
|
165
|
-
if hasattr(self._metrics_instance, "request_counter"):
|
|
166
|
-
request_counter = self._metrics_instance.request_counter
|
|
167
|
-
# Sum all request counts across all labels
|
|
168
|
-
total_requests = 0
|
|
169
|
-
for metric in request_counter.collect():
|
|
170
|
-
for sample in metric.samples:
|
|
171
|
-
if sample.name.endswith("_total"):
|
|
172
|
-
total_requests += sample.value
|
|
173
|
-
stats_data["requests_total"] = int(total_requests)
|
|
174
|
-
|
|
175
|
-
# Calculate last minute requests (difference from last snapshot)
|
|
176
|
-
if self._last_snapshot:
|
|
177
|
-
last_minute_requests = (
|
|
178
|
-
total_requests - self._last_snapshot.requests_total
|
|
179
|
-
)
|
|
180
|
-
stats_data["requests_last_minute"] = max(
|
|
181
|
-
0, int(last_minute_requests)
|
|
182
|
-
)
|
|
183
|
-
else:
|
|
184
|
-
stats_data["requests_last_minute"] = int(total_requests)
|
|
185
|
-
|
|
186
|
-
logger.debug(
|
|
187
|
-
"prometheus_requests_collected",
|
|
188
|
-
total_requests=total_requests,
|
|
189
|
-
requests_last_minute=stats_data["requests_last_minute"],
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Get response times from histogram
|
|
193
|
-
if hasattr(self._metrics_instance, "response_time"):
|
|
194
|
-
response_time = self._metrics_instance.response_time
|
|
195
|
-
# Get total count and sum for average calculation
|
|
196
|
-
total_count = 0
|
|
197
|
-
total_sum = 0
|
|
198
|
-
for metric in response_time.collect():
|
|
199
|
-
for sample in metric.samples:
|
|
200
|
-
if sample.name.endswith("_count"):
|
|
201
|
-
total_count += sample.value
|
|
202
|
-
elif sample.name.endswith("_sum"):
|
|
203
|
-
total_sum += sample.value
|
|
204
|
-
|
|
205
|
-
if total_count > 0:
|
|
206
|
-
avg_response_time_seconds = total_sum / total_count
|
|
207
|
-
stats_data["avg_response_time_ms"] = (
|
|
208
|
-
avg_response_time_seconds * 1000
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
# Calculate last minute average response time
|
|
212
|
-
if self._last_snapshot and self._last_snapshot.requests_total > 0:
|
|
213
|
-
last_minute_count = (
|
|
214
|
-
total_count - self._last_snapshot.requests_total
|
|
215
|
-
)
|
|
216
|
-
if last_minute_count > 0:
|
|
217
|
-
# Calculate the sum for just the last minute
|
|
218
|
-
last_minute_sum = total_sum - (
|
|
219
|
-
self._last_snapshot.requests_total
|
|
220
|
-
* self._last_snapshot.avg_response_time_ms
|
|
221
|
-
/ 1000
|
|
222
|
-
)
|
|
223
|
-
last_minute_avg = (
|
|
224
|
-
last_minute_sum / last_minute_count
|
|
225
|
-
) * 1000
|
|
226
|
-
stats_data["avg_response_time_last_minute_ms"] = float(
|
|
227
|
-
last_minute_avg
|
|
228
|
-
)
|
|
229
|
-
else:
|
|
230
|
-
stats_data["avg_response_time_last_minute_ms"] = 0.0
|
|
231
|
-
else:
|
|
232
|
-
stats_data["avg_response_time_last_minute_ms"] = stats_data[
|
|
233
|
-
"avg_response_time_ms"
|
|
234
|
-
]
|
|
235
|
-
|
|
236
|
-
# Get token counts from counter
|
|
237
|
-
if hasattr(self._metrics_instance, "token_counter"):
|
|
238
|
-
token_counter = self._metrics_instance.token_counter
|
|
239
|
-
tokens_input = 0
|
|
240
|
-
tokens_output = 0
|
|
241
|
-
for metric in token_counter.collect():
|
|
242
|
-
for sample in metric.samples:
|
|
243
|
-
if sample.name.endswith("_total"):
|
|
244
|
-
token_type = sample.labels.get("type", "")
|
|
245
|
-
if token_type == "input":
|
|
246
|
-
tokens_input += sample.value
|
|
247
|
-
elif token_type == "output":
|
|
248
|
-
tokens_output += sample.value
|
|
249
|
-
|
|
250
|
-
stats_data["tokens_input_total"] = int(tokens_input)
|
|
251
|
-
stats_data["tokens_output_total"] = int(tokens_output)
|
|
252
|
-
|
|
253
|
-
# Calculate last minute tokens
|
|
254
|
-
if self._last_snapshot:
|
|
255
|
-
last_minute_input = (
|
|
256
|
-
tokens_input - self._last_snapshot.tokens_input_total
|
|
257
|
-
)
|
|
258
|
-
last_minute_output = (
|
|
259
|
-
tokens_output - self._last_snapshot.tokens_output_total
|
|
260
|
-
)
|
|
261
|
-
stats_data["tokens_input_last_minute"] = max(
|
|
262
|
-
0, int(last_minute_input)
|
|
263
|
-
)
|
|
264
|
-
stats_data["tokens_output_last_minute"] = max(
|
|
265
|
-
0, int(last_minute_output)
|
|
266
|
-
)
|
|
267
|
-
else:
|
|
268
|
-
stats_data["tokens_input_last_minute"] = int(tokens_input)
|
|
269
|
-
stats_data["tokens_output_last_minute"] = int(tokens_output)
|
|
270
|
-
|
|
271
|
-
# Get cost from counter
|
|
272
|
-
if hasattr(self._metrics_instance, "cost_counter"):
|
|
273
|
-
cost_counter = self._metrics_instance.cost_counter
|
|
274
|
-
total_cost = 0
|
|
275
|
-
for metric in cost_counter.collect():
|
|
276
|
-
for sample in metric.samples:
|
|
277
|
-
if sample.name.endswith("_total"):
|
|
278
|
-
total_cost += sample.value
|
|
279
|
-
stats_data["cost_total_usd"] = float(total_cost)
|
|
280
|
-
|
|
281
|
-
# Calculate last minute cost
|
|
282
|
-
if self._last_snapshot:
|
|
283
|
-
last_minute_cost = total_cost - self._last_snapshot.cost_total_usd
|
|
284
|
-
stats_data["cost_last_minute_usd"] = max(
|
|
285
|
-
0.0, float(last_minute_cost)
|
|
286
|
-
)
|
|
287
|
-
else:
|
|
288
|
-
stats_data["cost_last_minute_usd"] = float(total_cost)
|
|
289
|
-
|
|
290
|
-
# Get error counts from counter
|
|
291
|
-
if hasattr(self._metrics_instance, "error_counter"):
|
|
292
|
-
error_counter = self._metrics_instance.error_counter
|
|
293
|
-
total_errors = 0
|
|
294
|
-
for metric in error_counter.collect():
|
|
295
|
-
for sample in metric.samples:
|
|
296
|
-
if sample.name.endswith("_total"):
|
|
297
|
-
total_errors += sample.value
|
|
298
|
-
stats_data["errors_total"] = int(total_errors)
|
|
299
|
-
|
|
300
|
-
# Calculate last minute errors
|
|
301
|
-
if self._last_snapshot:
|
|
302
|
-
last_minute_errors = total_errors - self._last_snapshot.errors_total
|
|
303
|
-
stats_data["errors_last_minute"] = max(0, int(last_minute_errors))
|
|
304
|
-
else:
|
|
305
|
-
stats_data["errors_last_minute"] = int(total_errors)
|
|
306
|
-
|
|
307
|
-
logger.debug(
|
|
308
|
-
"prometheus_stats_collected",
|
|
309
|
-
requests_total=stats_data["requests_total"],
|
|
310
|
-
requests_last_minute=stats_data["requests_last_minute"],
|
|
311
|
-
avg_response_time_ms=stats_data["avg_response_time_ms"],
|
|
312
|
-
tokens_input_total=stats_data["tokens_input_total"],
|
|
313
|
-
tokens_output_total=stats_data["tokens_output_total"],
|
|
314
|
-
cost_total_usd=stats_data["cost_total_usd"],
|
|
315
|
-
errors_total=stats_data["errors_total"],
|
|
316
|
-
active_requests=stats_data["active_requests"],
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
except Exception as e:
|
|
320
|
-
logger.debug("Failed to get metrics from Prometheus", error=str(e))
|
|
321
|
-
|
|
322
|
-
async def _collect_from_duckdb(
|
|
323
|
-
self, stats_data: dict[str, Any], current_time: float
|
|
324
|
-
) -> None:
|
|
325
|
-
"""Collect statistics from DuckDB storage."""
|
|
326
|
-
if not self._storage_instance:
|
|
327
|
-
return
|
|
328
|
-
|
|
329
|
-
try:
|
|
330
|
-
# Get overall analytics
|
|
331
|
-
overall_analytics = await self._storage_instance.get_analytics()
|
|
332
|
-
if overall_analytics and "summary" in overall_analytics:
|
|
333
|
-
summary = overall_analytics["summary"]
|
|
334
|
-
stats_data["requests_total"] = summary.get("total_requests", 0)
|
|
335
|
-
stats_data["avg_response_time_ms"] = summary.get("avg_duration_ms", 0.0)
|
|
336
|
-
stats_data["tokens_input_total"] = summary.get("total_tokens_input", 0)
|
|
337
|
-
stats_data["tokens_output_total"] = summary.get(
|
|
338
|
-
"total_tokens_output", 0
|
|
339
|
-
)
|
|
340
|
-
stats_data["cost_total_usd"] = summary.get("total_cost_usd", 0.0)
|
|
341
|
-
|
|
342
|
-
# Get last minute analytics
|
|
343
|
-
one_minute_ago = current_time - 60
|
|
344
|
-
last_minute_analytics = await self._storage_instance.get_analytics(
|
|
345
|
-
start_time=one_minute_ago,
|
|
346
|
-
end_time=current_time,
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
if last_minute_analytics and "summary" in last_minute_analytics:
|
|
350
|
-
last_minute_summary = last_minute_analytics["summary"]
|
|
351
|
-
stats_data["requests_last_minute"] = last_minute_summary.get(
|
|
352
|
-
"total_requests", 0
|
|
353
|
-
)
|
|
354
|
-
stats_data["avg_response_time_last_minute_ms"] = (
|
|
355
|
-
last_minute_summary.get("avg_duration_ms", 0.0)
|
|
356
|
-
)
|
|
357
|
-
stats_data["tokens_input_last_minute"] = last_minute_summary.get(
|
|
358
|
-
"total_tokens_input", 0
|
|
359
|
-
)
|
|
360
|
-
stats_data["tokens_output_last_minute"] = last_minute_summary.get(
|
|
361
|
-
"total_tokens_output", 0
|
|
362
|
-
)
|
|
363
|
-
stats_data["cost_last_minute_usd"] = last_minute_summary.get(
|
|
364
|
-
"total_cost_usd", 0.0
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
# Get top model from last minute data
|
|
368
|
-
await self._get_top_model(stats_data, one_minute_ago, current_time)
|
|
369
|
-
|
|
370
|
-
except Exception as e:
|
|
371
|
-
logger.debug("Failed to collect from DuckDB", error=str(e))
|
|
372
|
-
|
|
373
|
-
async def _get_top_model(
|
|
374
|
-
self, stats_data: dict[str, Any], start_time: float, end_time: float
|
|
375
|
-
) -> None:
|
|
376
|
-
"""Get the most used model in the time period."""
|
|
377
|
-
if not self._storage_instance:
|
|
378
|
-
return
|
|
379
|
-
|
|
380
|
-
try:
|
|
381
|
-
# Query for model usage
|
|
382
|
-
sql = """
|
|
383
|
-
SELECT model, COUNT(*) as request_count
|
|
384
|
-
FROM access_logs
|
|
385
|
-
WHERE timestamp >= ? AND timestamp <= ?
|
|
386
|
-
GROUP BY model
|
|
387
|
-
ORDER BY request_count DESC
|
|
388
|
-
LIMIT 1
|
|
389
|
-
"""
|
|
390
|
-
|
|
391
|
-
start_dt = datetime.fromtimestamp(start_time)
|
|
392
|
-
end_dt = datetime.fromtimestamp(end_time)
|
|
393
|
-
|
|
394
|
-
results = await self._storage_instance.query(
|
|
395
|
-
sql, [start_dt, end_dt], limit=1
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
if results:
|
|
399
|
-
top_model_data = results[0]
|
|
400
|
-
stats_data["top_model"] = top_model_data.get("model", "unknown")
|
|
401
|
-
request_count = top_model_data.get("request_count", 0)
|
|
402
|
-
|
|
403
|
-
if stats_data["requests_last_minute"] > 0:
|
|
404
|
-
stats_data["top_model_percentage"] = (
|
|
405
|
-
request_count / stats_data["requests_last_minute"]
|
|
406
|
-
) * 100
|
|
407
|
-
else:
|
|
408
|
-
stats_data["top_model_percentage"] = 0.0
|
|
409
|
-
|
|
410
|
-
except Exception as e:
|
|
411
|
-
logger.debug("Failed to get top model", error=str(e))
|
|
412
|
-
|
|
413
|
-
def _has_meaningful_activity(self, snapshot: StatsSnapshot) -> bool:
|
|
414
|
-
"""
|
|
415
|
-
Check if there is meaningful activity to report.
|
|
416
|
-
|
|
417
|
-
Args:
|
|
418
|
-
snapshot: Stats snapshot to check
|
|
419
|
-
|
|
420
|
-
Returns:
|
|
421
|
-
True if there is meaningful activity, False otherwise
|
|
422
|
-
"""
|
|
423
|
-
# Show stats if there are requests in the last minute
|
|
424
|
-
if snapshot.requests_last_minute > 0:
|
|
425
|
-
return True
|
|
426
|
-
|
|
427
|
-
# Show stats if there are currently active requests
|
|
428
|
-
if snapshot.active_requests > 0:
|
|
429
|
-
return True
|
|
430
|
-
|
|
431
|
-
# Show stats if there are any errors in the last minute
|
|
432
|
-
if snapshot.errors_last_minute > 0:
|
|
433
|
-
return True
|
|
434
|
-
|
|
435
|
-
# Show stats if there are any total requests (for the first time)
|
|
436
|
-
return snapshot.requests_total > 0 and self._last_snapshot is None
|
|
437
|
-
|
|
438
|
-
def format_stats(self, snapshot: StatsSnapshot) -> str:
|
|
439
|
-
"""
|
|
440
|
-
Format stats snapshot for display.
|
|
441
|
-
|
|
442
|
-
Args:
|
|
443
|
-
snapshot: Stats snapshot to format
|
|
444
|
-
|
|
445
|
-
Returns:
|
|
446
|
-
Formatted stats string
|
|
447
|
-
"""
|
|
448
|
-
format_type = self.settings.stats_printing_format
|
|
449
|
-
|
|
450
|
-
if format_type == "json":
|
|
451
|
-
return self._format_json(snapshot)
|
|
452
|
-
elif format_type == "rich":
|
|
453
|
-
return self._format_rich(snapshot)
|
|
454
|
-
elif format_type == "log":
|
|
455
|
-
return self._format_log(snapshot)
|
|
456
|
-
else: # console (default)
|
|
457
|
-
return self._format_console(snapshot)
|
|
458
|
-
|
|
459
|
-
def _format_console(self, snapshot: StatsSnapshot) -> str:
|
|
460
|
-
"""Format stats for console output."""
|
|
461
|
-
timestamp_str = snapshot.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
462
|
-
|
|
463
|
-
# Format response times
|
|
464
|
-
avg_response_str = f"{snapshot.avg_response_time_ms:.1f}ms"
|
|
465
|
-
avg_response_last_min_str = f"{snapshot.avg_response_time_last_minute_ms:.1f}ms"
|
|
466
|
-
|
|
467
|
-
# Format costs
|
|
468
|
-
cost_total_str = f"${snapshot.cost_total_usd:.4f}"
|
|
469
|
-
cost_last_min_str = f"${snapshot.cost_last_minute_usd:.4f}"
|
|
470
|
-
|
|
471
|
-
# Format top model percentage
|
|
472
|
-
top_model_str = f"{snapshot.top_model} ({snapshot.top_model_percentage:.1f}%)"
|
|
473
|
-
|
|
474
|
-
return f"""[{timestamp_str}] METRICS SUMMARY
|
|
475
|
-
├─ Requests: {snapshot.requests_last_minute} (last min) / {snapshot.requests_total} (total)
|
|
476
|
-
├─ Avg Response: {avg_response_last_min_str} (last min) / {avg_response_str} (overall)
|
|
477
|
-
├─ Tokens: {snapshot.tokens_input_last_minute:,} in / {snapshot.tokens_output_last_minute:,} out (last min)
|
|
478
|
-
├─ Cost: {cost_last_min_str} (last min) / {cost_total_str} (total)
|
|
479
|
-
├─ Errors: {snapshot.errors_last_minute} (last min) / {snapshot.errors_total} (total)
|
|
480
|
-
├─ Active: {snapshot.active_requests} requests
|
|
481
|
-
└─ Top Model: {top_model_str}"""
|
|
482
|
-
|
|
483
|
-
def _format_json(self, snapshot: StatsSnapshot) -> str:
|
|
484
|
-
"""Format stats for JSON output."""
|
|
485
|
-
data = {
|
|
486
|
-
"timestamp": snapshot.timestamp.isoformat(),
|
|
487
|
-
"requests": {
|
|
488
|
-
"last_minute": snapshot.requests_last_minute,
|
|
489
|
-
"total": snapshot.requests_total,
|
|
490
|
-
},
|
|
491
|
-
"response_time_ms": {
|
|
492
|
-
"last_minute": snapshot.avg_response_time_last_minute_ms,
|
|
493
|
-
"overall": snapshot.avg_response_time_ms,
|
|
494
|
-
},
|
|
495
|
-
"tokens": {
|
|
496
|
-
"input_last_minute": snapshot.tokens_input_last_minute,
|
|
497
|
-
"output_last_minute": snapshot.tokens_output_last_minute,
|
|
498
|
-
"input_total": snapshot.tokens_input_total,
|
|
499
|
-
"output_total": snapshot.tokens_output_total,
|
|
500
|
-
},
|
|
501
|
-
"cost_usd": {
|
|
502
|
-
"last_minute": snapshot.cost_last_minute_usd,
|
|
503
|
-
"total": snapshot.cost_total_usd,
|
|
504
|
-
},
|
|
505
|
-
"errors": {
|
|
506
|
-
"last_minute": snapshot.errors_last_minute,
|
|
507
|
-
"total": snapshot.errors_total,
|
|
508
|
-
},
|
|
509
|
-
"active_requests": snapshot.active_requests,
|
|
510
|
-
"top_model": {
|
|
511
|
-
"name": snapshot.top_model,
|
|
512
|
-
"percentage": snapshot.top_model_percentage,
|
|
513
|
-
},
|
|
514
|
-
}
|
|
515
|
-
return json.dumps(data, indent=2)
|
|
516
|
-
|
|
517
|
-
def _format_rich(self, snapshot: StatsSnapshot) -> str:
|
|
518
|
-
"""Format stats for rich console output with colors and styling."""
|
|
519
|
-
try:
|
|
520
|
-
# Try to import rich for enhanced formatting
|
|
521
|
-
from io import StringIO
|
|
522
|
-
|
|
523
|
-
from rich import box
|
|
524
|
-
from rich.console import Console
|
|
525
|
-
from rich.table import Table
|
|
526
|
-
|
|
527
|
-
output_buffer = StringIO()
|
|
528
|
-
console = Console(file=output_buffer, width=80, force_terminal=True)
|
|
529
|
-
timestamp_str = snapshot.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
530
|
-
|
|
531
|
-
# Create main stats table
|
|
532
|
-
table = Table(title=f"METRICS SUMMARY - {timestamp_str}", box=box.ROUNDED)
|
|
533
|
-
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
534
|
-
table.add_column("Last Minute", style="yellow", justify="right")
|
|
535
|
-
table.add_column("Total", style="green", justify="right")
|
|
536
|
-
|
|
537
|
-
# Add rows with formatted data
|
|
538
|
-
table.add_row(
|
|
539
|
-
"Requests",
|
|
540
|
-
f"{snapshot.requests_last_minute:,}",
|
|
541
|
-
f"{snapshot.requests_total:,}",
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
table.add_row(
|
|
545
|
-
"Avg Response",
|
|
546
|
-
f"{snapshot.avg_response_time_last_minute_ms:.1f}ms",
|
|
547
|
-
f"{snapshot.avg_response_time_ms:.1f}ms",
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
table.add_row(
|
|
551
|
-
"Tokens In",
|
|
552
|
-
f"{snapshot.tokens_input_last_minute:,}",
|
|
553
|
-
f"{snapshot.tokens_input_total:,}",
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
table.add_row(
|
|
557
|
-
"Tokens Out",
|
|
558
|
-
f"{snapshot.tokens_output_last_minute:,}",
|
|
559
|
-
f"{snapshot.tokens_output_total:,}",
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
table.add_row(
|
|
563
|
-
"Cost",
|
|
564
|
-
f"${snapshot.cost_last_minute_usd:.4f}",
|
|
565
|
-
f"${snapshot.cost_total_usd:.4f}",
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
table.add_row(
|
|
569
|
-
"Errors",
|
|
570
|
-
f"{snapshot.errors_last_minute}",
|
|
571
|
-
f"{snapshot.errors_total}",
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
# Add single-column rows
|
|
575
|
-
table.add_row("", "", "") # Separator
|
|
576
|
-
table.add_row("Active Requests", f"{snapshot.active_requests}", "")
|
|
577
|
-
|
|
578
|
-
table.add_row(
|
|
579
|
-
"Top Model",
|
|
580
|
-
f"{snapshot.top_model}",
|
|
581
|
-
f"({snapshot.top_model_percentage:.1f}%)",
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
console.print(table)
|
|
585
|
-
output = output_buffer.getvalue()
|
|
586
|
-
output_buffer.close()
|
|
587
|
-
|
|
588
|
-
return output.strip()
|
|
589
|
-
|
|
590
|
-
except ImportError:
|
|
591
|
-
# Fallback to console format if rich is not available
|
|
592
|
-
logger.warning("Rich not available, falling back to console format")
|
|
593
|
-
return self._format_console(snapshot)
|
|
594
|
-
except Exception as e:
|
|
595
|
-
logger.warning(
|
|
596
|
-
f"Rich formatting failed: {e}, falling back to console format"
|
|
597
|
-
)
|
|
598
|
-
return self._format_console(snapshot)
|
|
599
|
-
|
|
600
|
-
def _format_log(self, snapshot: StatsSnapshot) -> str:
|
|
601
|
-
"""Format stats for structured logging output."""
|
|
602
|
-
timestamp_str = snapshot.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
603
|
-
|
|
604
|
-
# Create a structured log entry
|
|
605
|
-
log_data = {
|
|
606
|
-
"timestamp": timestamp_str,
|
|
607
|
-
"event": "metrics_summary",
|
|
608
|
-
"requests": {
|
|
609
|
-
"last_minute": snapshot.requests_last_minute,
|
|
610
|
-
"total": snapshot.requests_total,
|
|
611
|
-
},
|
|
612
|
-
"response_time_ms": {
|
|
613
|
-
"last_minute_avg": snapshot.avg_response_time_last_minute_ms,
|
|
614
|
-
"overall_avg": snapshot.avg_response_time_ms,
|
|
615
|
-
},
|
|
616
|
-
"tokens": {
|
|
617
|
-
"input_last_minute": snapshot.tokens_input_last_minute,
|
|
618
|
-
"output_last_minute": snapshot.tokens_output_last_minute,
|
|
619
|
-
"input_total": snapshot.tokens_input_total,
|
|
620
|
-
"output_total": snapshot.tokens_output_total,
|
|
621
|
-
},
|
|
622
|
-
"cost_usd": {
|
|
623
|
-
"last_minute": snapshot.cost_last_minute_usd,
|
|
624
|
-
"total": snapshot.cost_total_usd,
|
|
625
|
-
},
|
|
626
|
-
"errors": {
|
|
627
|
-
"last_minute": snapshot.errors_last_minute,
|
|
628
|
-
"total": snapshot.errors_total,
|
|
629
|
-
},
|
|
630
|
-
"active_requests": snapshot.active_requests,
|
|
631
|
-
"top_model": {
|
|
632
|
-
"name": snapshot.top_model,
|
|
633
|
-
"percentage": snapshot.top_model_percentage,
|
|
634
|
-
},
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
# Format as a log line with key=value pairs
|
|
638
|
-
log_parts = [f"[{timestamp_str}]", "event=metrics_summary"]
|
|
639
|
-
|
|
640
|
-
log_parts.extend(
|
|
641
|
-
[
|
|
642
|
-
f"requests_last_min={snapshot.requests_last_minute}",
|
|
643
|
-
f"requests_total={snapshot.requests_total}",
|
|
644
|
-
f"avg_response_ms={snapshot.avg_response_time_ms:.1f}",
|
|
645
|
-
f"avg_response_last_min_ms={snapshot.avg_response_time_last_minute_ms:.1f}",
|
|
646
|
-
f"tokens_in_last_min={snapshot.tokens_input_last_minute}",
|
|
647
|
-
f"tokens_out_last_min={snapshot.tokens_output_last_minute}",
|
|
648
|
-
f"tokens_in_total={snapshot.tokens_input_total}",
|
|
649
|
-
f"tokens_out_total={snapshot.tokens_output_total}",
|
|
650
|
-
f"cost_last_min_usd={snapshot.cost_last_minute_usd:.4f}",
|
|
651
|
-
f"cost_total_usd={snapshot.cost_total_usd:.4f}",
|
|
652
|
-
f"errors_last_min={snapshot.errors_last_minute}",
|
|
653
|
-
f"errors_total={snapshot.errors_total}",
|
|
654
|
-
f"active_requests={snapshot.active_requests}",
|
|
655
|
-
f"top_model={snapshot.top_model}",
|
|
656
|
-
f"top_model_pct={snapshot.top_model_percentage:.1f}",
|
|
657
|
-
]
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
return " ".join(log_parts)
|
|
661
|
-
|
|
662
|
-
async def print_stats(self) -> None:
|
|
663
|
-
"""Collect and print current statistics."""
|
|
664
|
-
try:
|
|
665
|
-
snapshot = await self.collect_stats()
|
|
666
|
-
|
|
667
|
-
# Only print stats if there is meaningful activity
|
|
668
|
-
if self._has_meaningful_activity(snapshot):
|
|
669
|
-
formatted_stats = self.format_stats(snapshot)
|
|
670
|
-
|
|
671
|
-
# Print to stdout for console visibility
|
|
672
|
-
print(formatted_stats)
|
|
673
|
-
|
|
674
|
-
# Also log for structured logging
|
|
675
|
-
logger.info(
|
|
676
|
-
"stats_printed",
|
|
677
|
-
requests_last_minute=snapshot.requests_last_minute,
|
|
678
|
-
requests_total=snapshot.requests_total,
|
|
679
|
-
avg_response_time_ms=snapshot.avg_response_time_ms,
|
|
680
|
-
cost_total_usd=snapshot.cost_total_usd,
|
|
681
|
-
active_requests=snapshot.active_requests,
|
|
682
|
-
top_model=snapshot.top_model,
|
|
683
|
-
)
|
|
684
|
-
else:
|
|
685
|
-
logger.debug(
|
|
686
|
-
"stats_skipped_no_activity",
|
|
687
|
-
requests_last_minute=snapshot.requests_last_minute,
|
|
688
|
-
requests_total=snapshot.requests_total,
|
|
689
|
-
active_requests=snapshot.active_requests,
|
|
690
|
-
)
|
|
691
|
-
|
|
692
|
-
except Exception as e:
|
|
693
|
-
logger.error("Failed to print stats", error=str(e), exc_info=True)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
# Global stats collector instance
|
|
697
|
-
_global_stats_collector: StatsCollector | None = None
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
def get_stats_collector(
|
|
701
|
-
settings: ObservabilitySettings | None = None,
|
|
702
|
-
metrics_instance: Any | None = None,
|
|
703
|
-
storage_instance: Any | None = None,
|
|
704
|
-
) -> StatsCollector:
|
|
705
|
-
"""
|
|
706
|
-
Get or create global stats collector instance.
|
|
707
|
-
|
|
708
|
-
Args:
|
|
709
|
-
settings: Observability settings
|
|
710
|
-
metrics_instance: Metrics instance for dependency injection
|
|
711
|
-
storage_instance: Storage instance for dependency injection
|
|
712
|
-
|
|
713
|
-
Returns:
|
|
714
|
-
StatsCollector instance
|
|
715
|
-
"""
|
|
716
|
-
global _global_stats_collector
|
|
717
|
-
|
|
718
|
-
if _global_stats_collector is None:
|
|
719
|
-
if settings is None:
|
|
720
|
-
from ccproxy.config.settings import get_settings
|
|
721
|
-
|
|
722
|
-
settings = get_settings().observability
|
|
723
|
-
|
|
724
|
-
if metrics_instance is None:
|
|
725
|
-
try:
|
|
726
|
-
from .metrics import get_metrics
|
|
727
|
-
|
|
728
|
-
metrics_instance = get_metrics()
|
|
729
|
-
except Exception as e:
|
|
730
|
-
logger.warning("Failed to get metrics instance", error=str(e))
|
|
731
|
-
|
|
732
|
-
if storage_instance is None:
|
|
733
|
-
try:
|
|
734
|
-
from .storage.duckdb_simple import SimpleDuckDBStorage
|
|
735
|
-
|
|
736
|
-
storage_instance = SimpleDuckDBStorage(settings.duckdb_path)
|
|
737
|
-
# Note: Storage needs to be initialized before use
|
|
738
|
-
except Exception as e:
|
|
739
|
-
logger.warning("Failed to get storage instance", error=str(e))
|
|
740
|
-
|
|
741
|
-
_global_stats_collector = StatsCollector(
|
|
742
|
-
settings=settings,
|
|
743
|
-
metrics_instance=metrics_instance,
|
|
744
|
-
storage_instance=storage_instance,
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
return _global_stats_collector
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
def reset_stats_collector() -> None:
|
|
751
|
-
"""Reset global stats collector instance (mainly for testing)."""
|
|
752
|
-
global _global_stats_collector
|
|
753
|
-
_global_stats_collector = None
|