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,169 @@
|
|
|
1
|
+
"""Pricing 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
|
+
PluginManifest,
|
|
8
|
+
SystemPluginFactory,
|
|
9
|
+
SystemPluginRuntime,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .config import PricingConfig
|
|
13
|
+
from .service import PricingService
|
|
14
|
+
from .tasks import PricingCacheUpdateTask
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = get_plugin_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PricingRuntime(SystemPluginRuntime):
|
|
21
|
+
"""Runtime for pricing plugin."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, manifest: PluginManifest):
|
|
24
|
+
"""Initialize runtime."""
|
|
25
|
+
super().__init__(manifest)
|
|
26
|
+
self.config: PricingConfig | None = None
|
|
27
|
+
self.service: PricingService | None = None
|
|
28
|
+
self.update_task: PricingCacheUpdateTask | None = None
|
|
29
|
+
|
|
30
|
+
async def _on_initialize(self) -> None:
|
|
31
|
+
"""Initialize the pricing plugin."""
|
|
32
|
+
if not self.context:
|
|
33
|
+
raise RuntimeError("Context not set")
|
|
34
|
+
|
|
35
|
+
# Get configuration
|
|
36
|
+
config = self.context.get("config")
|
|
37
|
+
if not isinstance(config, PricingConfig):
|
|
38
|
+
logger.debug("plugin_no_config_using_defaults", category="plugin")
|
|
39
|
+
# Use default config if none provided
|
|
40
|
+
self.config = PricingConfig()
|
|
41
|
+
else:
|
|
42
|
+
self.config = config
|
|
43
|
+
|
|
44
|
+
logger.debug("initializing_pricing_plugin", enabled=self.config.enabled)
|
|
45
|
+
|
|
46
|
+
# Create pricing service
|
|
47
|
+
self.service = PricingService(self.config)
|
|
48
|
+
|
|
49
|
+
if self.config.enabled:
|
|
50
|
+
# Initialize the service
|
|
51
|
+
await self.service.initialize()
|
|
52
|
+
|
|
53
|
+
# Register service with plugin registry
|
|
54
|
+
plugin_registry = self.context.get("plugin_registry")
|
|
55
|
+
if plugin_registry:
|
|
56
|
+
plugin_registry.register_service(
|
|
57
|
+
"pricing", self.service, self.manifest.name
|
|
58
|
+
)
|
|
59
|
+
logger.debug("pricing_service_registered")
|
|
60
|
+
|
|
61
|
+
# Create and start pricing update task
|
|
62
|
+
interval_seconds = self.config.update_interval_hours * 3600
|
|
63
|
+
self.update_task = PricingCacheUpdateTask(
|
|
64
|
+
name="pricing_cache_update",
|
|
65
|
+
interval_seconds=interval_seconds,
|
|
66
|
+
pricing_service=self.service,
|
|
67
|
+
enabled=self.config.auto_update,
|
|
68
|
+
force_refresh_on_startup=self.config.force_refresh_on_startup,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
await self.update_task.start()
|
|
72
|
+
logger.debug(
|
|
73
|
+
"pricing_plugin_initialized",
|
|
74
|
+
update_interval_hours=self.config.update_interval_hours,
|
|
75
|
+
auto_update=self.config.auto_update,
|
|
76
|
+
force_refresh_on_startup=self.config.force_refresh_on_startup,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
logger.debug("pricing_plugin_disabled")
|
|
80
|
+
|
|
81
|
+
async def _on_shutdown(self) -> None:
|
|
82
|
+
"""Shutdown the plugin and cleanup resources."""
|
|
83
|
+
logger.debug("shutting_down_pricing_plugin")
|
|
84
|
+
|
|
85
|
+
# Stop the update task
|
|
86
|
+
if self.update_task:
|
|
87
|
+
await self.update_task.stop()
|
|
88
|
+
|
|
89
|
+
logger.debug("pricing_plugin_shutdown_complete")
|
|
90
|
+
|
|
91
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
92
|
+
"""Get health check details."""
|
|
93
|
+
try:
|
|
94
|
+
base_health = {
|
|
95
|
+
"type": "system",
|
|
96
|
+
"initialized": self.initialized,
|
|
97
|
+
"enabled": self.config.enabled if self.config else False,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if not self.config or not self.config.enabled:
|
|
101
|
+
return base_health
|
|
102
|
+
|
|
103
|
+
# Add service-specific health info
|
|
104
|
+
health_details = base_health.copy()
|
|
105
|
+
|
|
106
|
+
if self.service:
|
|
107
|
+
cache_info = self.service.get_cache_info()
|
|
108
|
+
health_details.update(
|
|
109
|
+
{
|
|
110
|
+
"cache_valid": cache_info.get("valid", False),
|
|
111
|
+
"cache_age_hours": cache_info.get("age_hours"),
|
|
112
|
+
"cache_exists": cache_info.get("exists", False),
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if self.update_task:
|
|
117
|
+
task_status = self.update_task.get_status()
|
|
118
|
+
health_details.update(
|
|
119
|
+
{
|
|
120
|
+
"update_task_running": task_status["running"],
|
|
121
|
+
"consecutive_failures": task_status["consecutive_failures"],
|
|
122
|
+
"last_success_ago_seconds": task_status[
|
|
123
|
+
"last_success_ago_seconds"
|
|
124
|
+
],
|
|
125
|
+
"next_run_in_seconds": task_status["next_run_in_seconds"],
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return health_details
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error("health_check_failed", error=str(e))
|
|
133
|
+
return {
|
|
134
|
+
"type": "system",
|
|
135
|
+
"initialized": self.initialized,
|
|
136
|
+
"enabled": self.config.enabled if self.config else False,
|
|
137
|
+
"error": str(e),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def get_pricing_service(self) -> PricingService | None:
|
|
141
|
+
"""Get the pricing service instance."""
|
|
142
|
+
return self.service
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class PricingFactory(SystemPluginFactory):
|
|
146
|
+
"""Factory for pricing plugin."""
|
|
147
|
+
|
|
148
|
+
def __init__(self) -> None:
|
|
149
|
+
"""Initialize factory with manifest."""
|
|
150
|
+
# Create manifest with static declarations
|
|
151
|
+
manifest = PluginManifest(
|
|
152
|
+
name="pricing",
|
|
153
|
+
version="0.1.0",
|
|
154
|
+
description="Dynamic pricing plugin for AI model cost calculation",
|
|
155
|
+
is_provider=False,
|
|
156
|
+
config_class=PricingConfig,
|
|
157
|
+
provides=["pricing"], # This plugin provides the pricing service
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Initialize with manifest
|
|
161
|
+
super().__init__(manifest)
|
|
162
|
+
|
|
163
|
+
def create_runtime(self) -> PricingRuntime:
|
|
164
|
+
"""Create runtime instance."""
|
|
165
|
+
return PricingRuntime(self.manifest)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Export the factory instance
|
|
169
|
+
factory = PricingFactory()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Pricing service providing unified interface for pricing functionality."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
7
|
+
|
|
8
|
+
from .cache import PricingCache
|
|
9
|
+
from .config import PricingConfig
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
ModelPricingNotFoundError,
|
|
12
|
+
PricingDataNotLoadedError,
|
|
13
|
+
PricingServiceDisabledError,
|
|
14
|
+
)
|
|
15
|
+
from .loader import PricingLoader
|
|
16
|
+
from .models import ModelPricing, PricingData
|
|
17
|
+
from .updater import PricingUpdater
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = get_plugin_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PricingService:
|
|
24
|
+
"""Main service interface for pricing functionality."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: PricingConfig):
|
|
27
|
+
"""Initialize pricing service with configuration."""
|
|
28
|
+
self.config = config
|
|
29
|
+
self.cache = PricingCache(config)
|
|
30
|
+
self.loader = PricingLoader()
|
|
31
|
+
self.updater = PricingUpdater(self.cache, config)
|
|
32
|
+
self._current_pricing: PricingData | None = None
|
|
33
|
+
|
|
34
|
+
async def initialize(self) -> None:
|
|
35
|
+
"""Initialize the pricing service."""
|
|
36
|
+
if not self.config.enabled:
|
|
37
|
+
logger.info("pricing_service_disabled")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
logger.debug("pricing_service_initializing")
|
|
41
|
+
|
|
42
|
+
# Force refresh on startup if configured
|
|
43
|
+
if self.config.force_refresh_on_startup:
|
|
44
|
+
await self.force_refresh_pricing()
|
|
45
|
+
else:
|
|
46
|
+
# Load current pricing data
|
|
47
|
+
await self.get_current_pricing()
|
|
48
|
+
|
|
49
|
+
async def get_current_pricing(
|
|
50
|
+
self, force_refresh: bool = False
|
|
51
|
+
) -> PricingData | None:
|
|
52
|
+
"""Get current pricing data."""
|
|
53
|
+
if not self.config.enabled:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
if force_refresh or self._current_pricing is None:
|
|
57
|
+
self._current_pricing = await self.updater.get_current_pricing(
|
|
58
|
+
force_refresh
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return self._current_pricing
|
|
62
|
+
|
|
63
|
+
async def get_model_pricing(self, model_name: str) -> ModelPricing | None:
|
|
64
|
+
"""Get pricing for specific model."""
|
|
65
|
+
pricing_data = await self.get_current_pricing()
|
|
66
|
+
if pricing_data is None:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return pricing_data.get(model_name)
|
|
70
|
+
|
|
71
|
+
async def calculate_cost(
|
|
72
|
+
self,
|
|
73
|
+
model_name: str,
|
|
74
|
+
input_tokens: int = 0,
|
|
75
|
+
output_tokens: int = 0,
|
|
76
|
+
cache_read_tokens: int = 0,
|
|
77
|
+
cache_write_tokens: int = 0,
|
|
78
|
+
) -> Decimal:
|
|
79
|
+
"""Calculate cost for token usage.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
PricingServiceDisabledError: If pricing service is disabled
|
|
83
|
+
ModelPricingNotFoundError: If model pricing is not found
|
|
84
|
+
"""
|
|
85
|
+
if not self.config.enabled:
|
|
86
|
+
raise PricingServiceDisabledError()
|
|
87
|
+
|
|
88
|
+
model_pricing = await self.get_model_pricing(model_name)
|
|
89
|
+
if model_pricing is None:
|
|
90
|
+
raise ModelPricingNotFoundError(model_name)
|
|
91
|
+
|
|
92
|
+
# Calculate cost per million tokens, then scale to actual tokens
|
|
93
|
+
total_cost = Decimal("0")
|
|
94
|
+
|
|
95
|
+
if input_tokens > 0:
|
|
96
|
+
total_cost += (model_pricing.input * input_tokens) / Decimal("1000000")
|
|
97
|
+
|
|
98
|
+
if output_tokens > 0:
|
|
99
|
+
total_cost += (model_pricing.output * output_tokens) / Decimal("1000000")
|
|
100
|
+
|
|
101
|
+
if cache_read_tokens > 0:
|
|
102
|
+
total_cost += (model_pricing.cache_read * cache_read_tokens) / Decimal(
|
|
103
|
+
"1000000"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if cache_write_tokens > 0:
|
|
107
|
+
total_cost += (model_pricing.cache_write * cache_write_tokens) / Decimal(
|
|
108
|
+
"1000000"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return total_cost
|
|
112
|
+
|
|
113
|
+
def calculate_cost_sync(
|
|
114
|
+
self,
|
|
115
|
+
model_name: str,
|
|
116
|
+
input_tokens: int = 0,
|
|
117
|
+
output_tokens: int = 0,
|
|
118
|
+
cache_read_tokens: int = 0,
|
|
119
|
+
cache_write_tokens: int = 0,
|
|
120
|
+
) -> Decimal:
|
|
121
|
+
"""Calculate cost synchronously using cached pricing data.
|
|
122
|
+
|
|
123
|
+
This method uses the cached pricing data and doesn't make any async calls,
|
|
124
|
+
making it safe to use in streaming contexts where we can't await.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
PricingServiceDisabledError: If pricing service is disabled
|
|
128
|
+
PricingDataNotLoadedError: If pricing data is not loaded yet
|
|
129
|
+
ModelPricingNotFoundError: If model pricing is not found
|
|
130
|
+
"""
|
|
131
|
+
if not self.config.enabled:
|
|
132
|
+
raise PricingServiceDisabledError()
|
|
133
|
+
|
|
134
|
+
if self._current_pricing is None:
|
|
135
|
+
raise PricingDataNotLoadedError()
|
|
136
|
+
|
|
137
|
+
model_pricing = self._current_pricing.get(model_name)
|
|
138
|
+
if model_pricing is None:
|
|
139
|
+
raise ModelPricingNotFoundError(model_name)
|
|
140
|
+
|
|
141
|
+
# Calculate cost per million tokens, then scale to actual tokens
|
|
142
|
+
total_cost = Decimal("0")
|
|
143
|
+
|
|
144
|
+
if input_tokens > 0:
|
|
145
|
+
total_cost += (model_pricing.input * input_tokens) / Decimal("1000000")
|
|
146
|
+
|
|
147
|
+
if output_tokens > 0:
|
|
148
|
+
total_cost += (model_pricing.output * output_tokens) / Decimal("1000000")
|
|
149
|
+
|
|
150
|
+
if cache_read_tokens > 0:
|
|
151
|
+
total_cost += (model_pricing.cache_read * cache_read_tokens) / Decimal(
|
|
152
|
+
"1000000"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if cache_write_tokens > 0:
|
|
156
|
+
total_cost += (model_pricing.cache_write * cache_write_tokens) / Decimal(
|
|
157
|
+
"1000000"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return total_cost
|
|
161
|
+
|
|
162
|
+
async def force_refresh_pricing(self) -> bool:
|
|
163
|
+
"""Force refresh of pricing data."""
|
|
164
|
+
if not self.config.enabled:
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
success = await self.updater.force_refresh()
|
|
168
|
+
if success:
|
|
169
|
+
# Reload the current pricing data after successful refresh
|
|
170
|
+
self._current_pricing = await self.updater.get_current_pricing(
|
|
171
|
+
force_refresh=True
|
|
172
|
+
)
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
async def get_available_models(self) -> list[str]:
|
|
177
|
+
"""Get list of available models with pricing."""
|
|
178
|
+
pricing_data = await self.get_current_pricing()
|
|
179
|
+
if pricing_data is None:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
return pricing_data.model_names()
|
|
183
|
+
|
|
184
|
+
def get_cache_info(self) -> dict[str, Any]:
|
|
185
|
+
"""Get cache status information."""
|
|
186
|
+
return self.cache.get_cache_info()
|
|
187
|
+
|
|
188
|
+
async def clear_cache(self) -> bool:
|
|
189
|
+
"""Clear pricing cache."""
|
|
190
|
+
self._current_pricing = None
|
|
191
|
+
return self.cache.clear_cache()
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Pricing plugin scheduled tasks."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ccproxy.core.async_task_manager import create_managed_task
|
|
11
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
12
|
+
|
|
13
|
+
from .service import PricingService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_plugin_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseScheduledTask(ABC):
|
|
20
|
+
"""
|
|
21
|
+
Abstract base class for all scheduled tasks.
|
|
22
|
+
|
|
23
|
+
Provides common functionality for task lifecycle management, error handling,
|
|
24
|
+
and exponential backoff for failed executions.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
interval_seconds: float,
|
|
31
|
+
enabled: bool = True,
|
|
32
|
+
max_backoff_seconds: float = 300.0,
|
|
33
|
+
jitter_factor: float = 0.25,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Initialize scheduled task.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name: Human-readable task name
|
|
40
|
+
interval_seconds: Interval between task executions in seconds
|
|
41
|
+
enabled: Whether the task is enabled
|
|
42
|
+
max_backoff_seconds: Maximum backoff delay for failed tasks
|
|
43
|
+
jitter_factor: Jitter factor for backoff randomization (0.0-1.0)
|
|
44
|
+
"""
|
|
45
|
+
self.name = name
|
|
46
|
+
self.interval_seconds = max(1.0, interval_seconds)
|
|
47
|
+
self.enabled = enabled
|
|
48
|
+
self.max_backoff_seconds = max_backoff_seconds
|
|
49
|
+
self.jitter_factor = min(1.0, max(0.0, jitter_factor))
|
|
50
|
+
|
|
51
|
+
# Task state
|
|
52
|
+
self._task: asyncio.Task[None] | None = None
|
|
53
|
+
self._stop_event = asyncio.Event()
|
|
54
|
+
self._consecutive_failures = 0
|
|
55
|
+
self._last_success_time: float | None = None
|
|
56
|
+
self._next_run_time: float | None = None
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def run(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Execute the task logic.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if task completed successfully, False otherwise
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
async def setup(self) -> None: # noqa: B027
|
|
68
|
+
"""
|
|
69
|
+
Optional setup hook called before the task starts running.
|
|
70
|
+
|
|
71
|
+
Override this method to perform any initialization required by the task.
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
async def teardown(self) -> None: # noqa: B027
|
|
76
|
+
"""
|
|
77
|
+
Optional teardown hook called when the task stops.
|
|
78
|
+
|
|
79
|
+
Override this method to perform any cleanup required by the task.
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def _calculate_next_run_delay(self, failed: bool = False) -> float:
|
|
84
|
+
"""Calculate delay until next task execution with exponential backoff."""
|
|
85
|
+
if not failed:
|
|
86
|
+
# Normal interval with jitter
|
|
87
|
+
base_delay = self.interval_seconds
|
|
88
|
+
jitter = random.uniform(-self.jitter_factor, self.jitter_factor)
|
|
89
|
+
return float(base_delay * (1 + jitter))
|
|
90
|
+
|
|
91
|
+
# Exponential backoff for failures
|
|
92
|
+
backoff_factor = min(2**self._consecutive_failures, 32)
|
|
93
|
+
backoff_delay = min(
|
|
94
|
+
self.interval_seconds * backoff_factor, self.max_backoff_seconds
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Add jitter to prevent thundering herd
|
|
98
|
+
jitter = random.uniform(-self.jitter_factor, self.jitter_factor)
|
|
99
|
+
return float(backoff_delay * (1 + jitter))
|
|
100
|
+
|
|
101
|
+
async def _run_with_error_handling(self) -> bool:
|
|
102
|
+
"""Execute task with error handling and metrics."""
|
|
103
|
+
start_time = time.time()
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
success = await self.run()
|
|
107
|
+
|
|
108
|
+
if success:
|
|
109
|
+
self._consecutive_failures = 0
|
|
110
|
+
self._last_success_time = start_time
|
|
111
|
+
logger.debug(
|
|
112
|
+
"scheduled_task_success",
|
|
113
|
+
task_name=self.name,
|
|
114
|
+
duration=time.time() - start_time,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
self._consecutive_failures += 1
|
|
118
|
+
logger.warning(
|
|
119
|
+
"scheduled_task_failed",
|
|
120
|
+
task_name=self.name,
|
|
121
|
+
consecutive_failures=self._consecutive_failures,
|
|
122
|
+
duration=time.time() - start_time,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return success
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self._consecutive_failures += 1
|
|
129
|
+
logger.error(
|
|
130
|
+
"scheduled_task_error",
|
|
131
|
+
task_name=self.name,
|
|
132
|
+
error=str(e),
|
|
133
|
+
error_type=type(e).__name__,
|
|
134
|
+
consecutive_failures=self._consecutive_failures,
|
|
135
|
+
duration=time.time() - start_time,
|
|
136
|
+
exc_info=e,
|
|
137
|
+
)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
async def _task_loop(self) -> None:
|
|
141
|
+
"""Main task execution loop."""
|
|
142
|
+
logger.info("scheduled_task_starting", task_name=self.name)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Run setup
|
|
146
|
+
with contextlib.suppress(Exception):
|
|
147
|
+
await self.setup()
|
|
148
|
+
|
|
149
|
+
while not self._stop_event.is_set():
|
|
150
|
+
# Execute task
|
|
151
|
+
success = await self._run_with_error_handling()
|
|
152
|
+
|
|
153
|
+
# Calculate next run delay
|
|
154
|
+
delay = self._calculate_next_run_delay(failed=not success)
|
|
155
|
+
self._next_run_time = time.time() + delay
|
|
156
|
+
|
|
157
|
+
# Wait for next execution or stop event
|
|
158
|
+
try:
|
|
159
|
+
await asyncio.wait_for(self._stop_event.wait(), timeout=delay)
|
|
160
|
+
break # Stop event was set
|
|
161
|
+
except TimeoutError:
|
|
162
|
+
continue # Time to run again
|
|
163
|
+
|
|
164
|
+
finally:
|
|
165
|
+
# Run teardown
|
|
166
|
+
with contextlib.suppress(Exception):
|
|
167
|
+
await self.teardown()
|
|
168
|
+
|
|
169
|
+
logger.info("scheduled_task_stopped", task_name=self.name)
|
|
170
|
+
|
|
171
|
+
async def start(self) -> None:
|
|
172
|
+
"""Start the scheduled task."""
|
|
173
|
+
if not self.enabled:
|
|
174
|
+
logger.info("scheduled_task_disabled", task_name=self.name)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if self._task and not self._task.done():
|
|
178
|
+
logger.warning("scheduled_task_already_running", task_name=self.name)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
self._stop_event.clear()
|
|
182
|
+
self._task = await create_managed_task(
|
|
183
|
+
self._task_loop(), name=f"scheduled_task_{self.name}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def stop(self, timeout: float = 10.0) -> None:
|
|
187
|
+
"""Stop the scheduled task."""
|
|
188
|
+
if not self._task:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
logger.info("scheduled_task_stopping", task_name=self.name)
|
|
192
|
+
|
|
193
|
+
# Signal stop
|
|
194
|
+
self._stop_event.set()
|
|
195
|
+
|
|
196
|
+
# Wait for task to complete
|
|
197
|
+
try:
|
|
198
|
+
await asyncio.wait_for(self._task, timeout=timeout)
|
|
199
|
+
except TimeoutError:
|
|
200
|
+
logger.warning(
|
|
201
|
+
"scheduled_task_stop_timeout", task_name=self.name, timeout=timeout
|
|
202
|
+
)
|
|
203
|
+
if not self._task.done():
|
|
204
|
+
self._task.cancel()
|
|
205
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
206
|
+
await self._task
|
|
207
|
+
|
|
208
|
+
self._task = None
|
|
209
|
+
|
|
210
|
+
def is_running(self) -> bool:
|
|
211
|
+
"""Check if task is currently running."""
|
|
212
|
+
return self._task is not None and not self._task.done()
|
|
213
|
+
|
|
214
|
+
def get_status(self) -> dict[str, Any]:
|
|
215
|
+
"""Get current task status information."""
|
|
216
|
+
now = time.time()
|
|
217
|
+
return {
|
|
218
|
+
"name": self.name,
|
|
219
|
+
"enabled": self.enabled,
|
|
220
|
+
"running": self.is_running(),
|
|
221
|
+
"consecutive_failures": self._consecutive_failures,
|
|
222
|
+
"last_success_time": self._last_success_time,
|
|
223
|
+
"last_success_ago_seconds": (
|
|
224
|
+
now - self._last_success_time if self._last_success_time else None
|
|
225
|
+
),
|
|
226
|
+
"next_run_time": self._next_run_time,
|
|
227
|
+
"next_run_in_seconds": (
|
|
228
|
+
self._next_run_time - now if self._next_run_time else None
|
|
229
|
+
),
|
|
230
|
+
"interval_seconds": self.interval_seconds,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PricingCacheUpdateTask(BaseScheduledTask):
|
|
235
|
+
"""Task for updating pricing cache periodically."""
|
|
236
|
+
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
name: str,
|
|
240
|
+
interval_seconds: float,
|
|
241
|
+
pricing_service: PricingService,
|
|
242
|
+
enabled: bool = True,
|
|
243
|
+
force_refresh_on_startup: bool = False,
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Initialize pricing cache update task.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
name: Task name
|
|
250
|
+
interval_seconds: Interval between pricing updates
|
|
251
|
+
pricing_service: Pricing service instance
|
|
252
|
+
enabled: Whether task is enabled
|
|
253
|
+
force_refresh_on_startup: Whether to force refresh on first run
|
|
254
|
+
"""
|
|
255
|
+
super().__init__(
|
|
256
|
+
name=name,
|
|
257
|
+
interval_seconds=interval_seconds,
|
|
258
|
+
enabled=enabled,
|
|
259
|
+
)
|
|
260
|
+
self.pricing_service = pricing_service
|
|
261
|
+
self.force_refresh_on_startup = force_refresh_on_startup
|
|
262
|
+
self._first_run = True
|
|
263
|
+
|
|
264
|
+
async def run(self) -> bool:
|
|
265
|
+
"""Execute pricing cache update."""
|
|
266
|
+
try:
|
|
267
|
+
if not self.pricing_service.config.enabled:
|
|
268
|
+
logger.debug("pricing_service_disabled", task_name=self.name)
|
|
269
|
+
return True # Not a failure, just disabled
|
|
270
|
+
|
|
271
|
+
# Force refresh on first run if configured
|
|
272
|
+
force_refresh = self._first_run and self.force_refresh_on_startup
|
|
273
|
+
self._first_run = False
|
|
274
|
+
|
|
275
|
+
if force_refresh:
|
|
276
|
+
logger.info("pricing_update_force_refresh_startup", task_name=self.name)
|
|
277
|
+
success = await self.pricing_service.force_refresh_pricing()
|
|
278
|
+
else:
|
|
279
|
+
# Regular update check
|
|
280
|
+
pricing_data = await self.pricing_service.get_current_pricing(
|
|
281
|
+
force_refresh=False
|
|
282
|
+
)
|
|
283
|
+
success = pricing_data is not None
|
|
284
|
+
|
|
285
|
+
if success:
|
|
286
|
+
logger.debug("pricing_update_success", task_name=self.name)
|
|
287
|
+
else:
|
|
288
|
+
logger.warning("pricing_update_failed", task_name=self.name)
|
|
289
|
+
|
|
290
|
+
return success
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(
|
|
294
|
+
"pricing_update_task_error",
|
|
295
|
+
task_name=self.name,
|
|
296
|
+
error=str(e),
|
|
297
|
+
error_type=type(e).__name__,
|
|
298
|
+
exc_info=e,
|
|
299
|
+
)
|
|
300
|
+
return False
|