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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Storage backends for observability data."""
|
|
@@ -1,677 +0,0 @@
|
|
|
1
|
-
"""Simplified DuckDB storage for low-traffic environments.
|
|
2
|
-
|
|
3
|
-
This module provides a simple, direct DuckDB storage implementation without
|
|
4
|
-
connection pooling or batch processing. Suitable for dev environments with
|
|
5
|
-
low request rates (< 10 req/s).
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import asyncio
|
|
9
|
-
import time
|
|
10
|
-
from collections.abc import Sequence
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
import structlog
|
|
16
|
-
from sqlalchemy import text
|
|
17
|
-
from sqlalchemy.engine import Engine
|
|
18
|
-
from sqlmodel import Session, SQLModel, create_engine, desc, func, select
|
|
19
|
-
from typing_extensions import TypedDict
|
|
20
|
-
|
|
21
|
-
from .models import AccessLog
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
logger = structlog.get_logger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class AccessLogPayload(TypedDict, total=False):
|
|
28
|
-
"""TypedDict for access log data payloads.
|
|
29
|
-
|
|
30
|
-
Note: All fields are optional (total=False) to allow partial payloads.
|
|
31
|
-
The storage layer will provide sensible defaults for missing fields.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
# Core request identification
|
|
35
|
-
request_id: str
|
|
36
|
-
timestamp: int | float | datetime
|
|
37
|
-
|
|
38
|
-
# Request details
|
|
39
|
-
method: str
|
|
40
|
-
endpoint: str
|
|
41
|
-
path: str
|
|
42
|
-
query: str
|
|
43
|
-
client_ip: str
|
|
44
|
-
user_agent: str
|
|
45
|
-
|
|
46
|
-
# Service and model info
|
|
47
|
-
service_type: str
|
|
48
|
-
model: str
|
|
49
|
-
streaming: bool
|
|
50
|
-
|
|
51
|
-
# Response details
|
|
52
|
-
status_code: int
|
|
53
|
-
duration_ms: float
|
|
54
|
-
duration_seconds: float
|
|
55
|
-
|
|
56
|
-
# Token and cost tracking
|
|
57
|
-
tokens_input: int
|
|
58
|
-
tokens_output: int
|
|
59
|
-
cache_read_tokens: int
|
|
60
|
-
cache_write_tokens: int
|
|
61
|
-
cost_usd: float
|
|
62
|
-
cost_sdk_usd: float
|
|
63
|
-
num_turns: int # number of conversation turns
|
|
64
|
-
|
|
65
|
-
# Session context metadata
|
|
66
|
-
session_type: str # "session_pool" or "direct"
|
|
67
|
-
session_status: str # active, idle, connecting, etc.
|
|
68
|
-
session_age_seconds: float # how long session has been alive
|
|
69
|
-
session_message_count: int # number of messages in session
|
|
70
|
-
session_client_id: str # unique session client identifier
|
|
71
|
-
session_pool_enabled: bool # whether session pooling is enabled
|
|
72
|
-
session_idle_seconds: float # how long since last activity
|
|
73
|
-
session_error_count: int # number of errors in this session
|
|
74
|
-
session_is_new: bool # whether this is a newly created session
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class SimpleDuckDBStorage:
|
|
78
|
-
"""Simple DuckDB storage with queue-based writes to prevent deadlocks."""
|
|
79
|
-
|
|
80
|
-
def __init__(self, database_path: str | Path = "data/metrics.duckdb"):
|
|
81
|
-
"""Initialize simple DuckDB storage.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
database_path: Path to DuckDB database file
|
|
85
|
-
"""
|
|
86
|
-
self.database_path = Path(database_path)
|
|
87
|
-
self._engine: Engine | None = None
|
|
88
|
-
self._initialized: bool = False
|
|
89
|
-
self._write_queue: asyncio.Queue[AccessLogPayload] = asyncio.Queue()
|
|
90
|
-
self._background_worker_task: asyncio.Task[None] | None = None
|
|
91
|
-
self._shutdown_event = asyncio.Event()
|
|
92
|
-
|
|
93
|
-
async def initialize(self) -> None:
|
|
94
|
-
"""Initialize the storage backend."""
|
|
95
|
-
if self._initialized:
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
# Ensure data directory exists
|
|
100
|
-
self.database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
-
|
|
102
|
-
# Create SQLModel engine
|
|
103
|
-
self._engine = create_engine(f"duckdb:///{self.database_path}")
|
|
104
|
-
|
|
105
|
-
# Create schema using SQLModel (synchronous in main thread)
|
|
106
|
-
self._create_schema_sync()
|
|
107
|
-
|
|
108
|
-
# Start background worker for queue processing
|
|
109
|
-
self._background_worker_task = asyncio.create_task(
|
|
110
|
-
self._background_worker()
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
self._initialized = True
|
|
114
|
-
logger.debug(
|
|
115
|
-
"simple_duckdb_initialized", database_path=str(self.database_path)
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
except Exception as e:
|
|
119
|
-
logger.error("simple_duckdb_init_error", error=str(e), exc_info=True)
|
|
120
|
-
raise
|
|
121
|
-
|
|
122
|
-
def _create_schema_sync(self) -> None:
|
|
123
|
-
"""Create database schema using SQLModel (synchronous)."""
|
|
124
|
-
if not self._engine:
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
# Create tables using SQLModel metadata
|
|
129
|
-
SQLModel.metadata.create_all(self._engine)
|
|
130
|
-
logger.debug("duckdb_schema_created")
|
|
131
|
-
|
|
132
|
-
except Exception as e:
|
|
133
|
-
logger.error("simple_duckdb_schema_error", error=str(e))
|
|
134
|
-
raise
|
|
135
|
-
|
|
136
|
-
async def _ensure_query_column(self) -> None:
|
|
137
|
-
"""Ensure query column exists in the access_logs table."""
|
|
138
|
-
if not self._engine:
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
with Session(self._engine) as session:
|
|
143
|
-
# Check if query column exists
|
|
144
|
-
result = session.execute(
|
|
145
|
-
text(
|
|
146
|
-
"SELECT column_name FROM information_schema.columns WHERE table_name = 'access_logs' AND column_name = 'query'"
|
|
147
|
-
)
|
|
148
|
-
)
|
|
149
|
-
if not result.fetchone():
|
|
150
|
-
# Add query column if it doesn't exist
|
|
151
|
-
session.execute(
|
|
152
|
-
text(
|
|
153
|
-
"ALTER TABLE access_logs ADD COLUMN query VARCHAR DEFAULT ''"
|
|
154
|
-
)
|
|
155
|
-
)
|
|
156
|
-
session.commit()
|
|
157
|
-
logger.info("Added query column to access_logs table")
|
|
158
|
-
|
|
159
|
-
except Exception as e:
|
|
160
|
-
logger.warning("Failed to check/add query column", error=str(e))
|
|
161
|
-
# Continue without failing - the column might already exist or schema might be different
|
|
162
|
-
|
|
163
|
-
async def store_request(self, data: AccessLogPayload) -> bool:
|
|
164
|
-
"""Store a single request log entry asynchronously via queue.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
data: Request data to store
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
True if queued successfully
|
|
171
|
-
"""
|
|
172
|
-
if not self._initialized:
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
try:
|
|
176
|
-
# Add to queue for background processing
|
|
177
|
-
await self._write_queue.put(data)
|
|
178
|
-
return True
|
|
179
|
-
except Exception as e:
|
|
180
|
-
logger.error(
|
|
181
|
-
"queue_store_error",
|
|
182
|
-
error=str(e),
|
|
183
|
-
request_id=data.get("request_id"),
|
|
184
|
-
)
|
|
185
|
-
return False
|
|
186
|
-
|
|
187
|
-
async def _background_worker(self) -> None:
|
|
188
|
-
"""Background worker to process queued write operations sequentially."""
|
|
189
|
-
logger.debug("duckdb_background_worker_started")
|
|
190
|
-
|
|
191
|
-
while not self._shutdown_event.is_set():
|
|
192
|
-
try:
|
|
193
|
-
# Wait for either a queue item or shutdown with timeout
|
|
194
|
-
try:
|
|
195
|
-
data = await asyncio.wait_for(self._write_queue.get(), timeout=1.0)
|
|
196
|
-
except TimeoutError:
|
|
197
|
-
continue # Check shutdown event and continue
|
|
198
|
-
|
|
199
|
-
# Process the queued write operation synchronously
|
|
200
|
-
try:
|
|
201
|
-
success = self._store_request_sync(data)
|
|
202
|
-
if success:
|
|
203
|
-
logger.debug(
|
|
204
|
-
"queue_processed_successfully",
|
|
205
|
-
request_id=data.get("request_id"),
|
|
206
|
-
)
|
|
207
|
-
except Exception as e:
|
|
208
|
-
logger.error(
|
|
209
|
-
"background_worker_error",
|
|
210
|
-
error=str(e),
|
|
211
|
-
request_id=data.get("request_id"),
|
|
212
|
-
exc_info=True,
|
|
213
|
-
)
|
|
214
|
-
finally:
|
|
215
|
-
# Always mark the task as done, regardless of success/failure
|
|
216
|
-
self._write_queue.task_done()
|
|
217
|
-
|
|
218
|
-
except Exception as e:
|
|
219
|
-
logger.error(
|
|
220
|
-
"background_worker_unexpected_error",
|
|
221
|
-
error=str(e),
|
|
222
|
-
exc_info=True,
|
|
223
|
-
)
|
|
224
|
-
# Continue processing other items
|
|
225
|
-
|
|
226
|
-
# Process any remaining items in the queue during shutdown
|
|
227
|
-
logger.debug("processing_remaining_queue_items_on_shutdown")
|
|
228
|
-
while not self._write_queue.empty():
|
|
229
|
-
try:
|
|
230
|
-
# Get remaining items without timeout during shutdown
|
|
231
|
-
data = self._write_queue.get_nowait()
|
|
232
|
-
|
|
233
|
-
# Process the queued write operation synchronously
|
|
234
|
-
try:
|
|
235
|
-
success = self._store_request_sync(data)
|
|
236
|
-
if success:
|
|
237
|
-
logger.debug(
|
|
238
|
-
"shutdown_queue_processed_successfully",
|
|
239
|
-
request_id=data.get("request_id"),
|
|
240
|
-
)
|
|
241
|
-
except Exception as e:
|
|
242
|
-
logger.error(
|
|
243
|
-
"shutdown_background_worker_error",
|
|
244
|
-
error=str(e),
|
|
245
|
-
request_id=data.get("request_id"),
|
|
246
|
-
exc_info=True,
|
|
247
|
-
)
|
|
248
|
-
finally:
|
|
249
|
-
# Always mark the task as done, regardless of success/failure
|
|
250
|
-
self._write_queue.task_done()
|
|
251
|
-
|
|
252
|
-
except asyncio.QueueEmpty:
|
|
253
|
-
# No more items to process
|
|
254
|
-
break
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.error(
|
|
257
|
-
"shutdown_background_worker_unexpected_error",
|
|
258
|
-
error=str(e),
|
|
259
|
-
exc_info=True,
|
|
260
|
-
)
|
|
261
|
-
# Continue processing other items
|
|
262
|
-
|
|
263
|
-
logger.debug("duckdb_background_worker_stopped")
|
|
264
|
-
|
|
265
|
-
def _store_request_sync(self, data: AccessLogPayload) -> bool:
|
|
266
|
-
"""Synchronous version of store_request for thread pool execution."""
|
|
267
|
-
try:
|
|
268
|
-
# Convert Unix timestamp to datetime if needed
|
|
269
|
-
timestamp_value = data.get("timestamp", time.time())
|
|
270
|
-
if isinstance(timestamp_value, int | float):
|
|
271
|
-
timestamp_dt = datetime.fromtimestamp(timestamp_value)
|
|
272
|
-
else:
|
|
273
|
-
timestamp_dt = timestamp_value
|
|
274
|
-
|
|
275
|
-
# Create AccessLog object with type validation
|
|
276
|
-
access_log = AccessLog(
|
|
277
|
-
request_id=data.get("request_id", ""),
|
|
278
|
-
timestamp=timestamp_dt,
|
|
279
|
-
method=data.get("method", ""),
|
|
280
|
-
endpoint=data.get("endpoint", ""),
|
|
281
|
-
path=data.get("path", data.get("endpoint", "")),
|
|
282
|
-
query=data.get("query", ""),
|
|
283
|
-
client_ip=data.get("client_ip", ""),
|
|
284
|
-
user_agent=data.get("user_agent", ""),
|
|
285
|
-
service_type=data.get("service_type", ""),
|
|
286
|
-
model=data.get("model", ""),
|
|
287
|
-
streaming=data.get("streaming", False),
|
|
288
|
-
status_code=data.get("status_code", 200),
|
|
289
|
-
duration_ms=data.get("duration_ms", 0.0),
|
|
290
|
-
duration_seconds=data.get("duration_seconds", 0.0),
|
|
291
|
-
tokens_input=data.get("tokens_input", 0),
|
|
292
|
-
tokens_output=data.get("tokens_output", 0),
|
|
293
|
-
cache_read_tokens=data.get("cache_read_tokens", 0),
|
|
294
|
-
cache_write_tokens=data.get("cache_write_tokens", 0),
|
|
295
|
-
cost_usd=data.get("cost_usd", 0.0),
|
|
296
|
-
cost_sdk_usd=data.get("cost_sdk_usd", 0.0),
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
# Store using SQLModel session
|
|
300
|
-
with Session(self._engine) as session:
|
|
301
|
-
# Add new log entry (no merge needed as each request is unique)
|
|
302
|
-
session.add(access_log)
|
|
303
|
-
session.commit()
|
|
304
|
-
|
|
305
|
-
logger.info(
|
|
306
|
-
"simple_duckdb_store_success",
|
|
307
|
-
request_id=data.get("request_id"),
|
|
308
|
-
service_type=data.get("service_type", ""),
|
|
309
|
-
model=data.get("model", ""),
|
|
310
|
-
tokens_input=data.get("tokens_input", 0),
|
|
311
|
-
tokens_output=data.get("tokens_output", 0),
|
|
312
|
-
cost_usd=data.get("cost_usd", 0.0),
|
|
313
|
-
endpoint=data.get("endpoint", ""),
|
|
314
|
-
timestamp=timestamp_dt.isoformat() if timestamp_dt else None,
|
|
315
|
-
)
|
|
316
|
-
return True
|
|
317
|
-
|
|
318
|
-
except Exception as e:
|
|
319
|
-
logger.error(
|
|
320
|
-
"simple_duckdb_store_error",
|
|
321
|
-
error=str(e),
|
|
322
|
-
request_id=data.get("request_id"),
|
|
323
|
-
)
|
|
324
|
-
return False
|
|
325
|
-
|
|
326
|
-
async def store_batch(self, metrics: Sequence[AccessLogPayload]) -> bool:
|
|
327
|
-
"""Store a batch of metrics efficiently.
|
|
328
|
-
|
|
329
|
-
Args:
|
|
330
|
-
metrics: List of metric data to store
|
|
331
|
-
|
|
332
|
-
Returns:
|
|
333
|
-
True if batch stored successfully
|
|
334
|
-
"""
|
|
335
|
-
if not self._initialized or not metrics or not self._engine:
|
|
336
|
-
return False
|
|
337
|
-
|
|
338
|
-
try:
|
|
339
|
-
# Store using SQLModel with upsert behavior
|
|
340
|
-
with Session(self._engine) as session:
|
|
341
|
-
for metric in metrics:
|
|
342
|
-
# Convert Unix timestamp to datetime if needed
|
|
343
|
-
timestamp_value = metric.get("timestamp", time.time())
|
|
344
|
-
if isinstance(timestamp_value, int | float):
|
|
345
|
-
timestamp_dt = datetime.fromtimestamp(timestamp_value)
|
|
346
|
-
else:
|
|
347
|
-
timestamp_dt = timestamp_value
|
|
348
|
-
|
|
349
|
-
# Create AccessLog object with type validation
|
|
350
|
-
access_log = AccessLog(
|
|
351
|
-
request_id=metric.get("request_id", ""),
|
|
352
|
-
timestamp=timestamp_dt,
|
|
353
|
-
method=metric.get("method", ""),
|
|
354
|
-
endpoint=metric.get("endpoint", ""),
|
|
355
|
-
path=metric.get("path", metric.get("endpoint", "")),
|
|
356
|
-
query=metric.get("query", ""),
|
|
357
|
-
client_ip=metric.get("client_ip", ""),
|
|
358
|
-
user_agent=metric.get("user_agent", ""),
|
|
359
|
-
service_type=metric.get("service_type", ""),
|
|
360
|
-
model=metric.get("model", ""),
|
|
361
|
-
streaming=metric.get("streaming", False),
|
|
362
|
-
status_code=metric.get("status_code", 200),
|
|
363
|
-
duration_ms=metric.get("duration_ms", 0.0),
|
|
364
|
-
duration_seconds=metric.get("duration_seconds", 0.0),
|
|
365
|
-
tokens_input=metric.get("tokens_input", 0),
|
|
366
|
-
tokens_output=metric.get("tokens_output", 0),
|
|
367
|
-
cache_read_tokens=metric.get("cache_read_tokens", 0),
|
|
368
|
-
cache_write_tokens=metric.get("cache_write_tokens", 0),
|
|
369
|
-
cost_usd=metric.get("cost_usd", 0.0),
|
|
370
|
-
cost_sdk_usd=metric.get("cost_sdk_usd", 0.0),
|
|
371
|
-
)
|
|
372
|
-
# Use merge to handle potential duplicates
|
|
373
|
-
session.merge(access_log)
|
|
374
|
-
|
|
375
|
-
session.commit()
|
|
376
|
-
|
|
377
|
-
logger.info(
|
|
378
|
-
"simple_duckdb_batch_store_success",
|
|
379
|
-
batch_size=len(metrics),
|
|
380
|
-
service_types=[
|
|
381
|
-
m.get("service_type", "") for m in metrics[:3]
|
|
382
|
-
], # First 3 for sampling
|
|
383
|
-
request_ids=[
|
|
384
|
-
m.get("request_id", "") for m in metrics[:3]
|
|
385
|
-
], # First 3 for sampling
|
|
386
|
-
)
|
|
387
|
-
return True
|
|
388
|
-
|
|
389
|
-
except Exception as e:
|
|
390
|
-
logger.error(
|
|
391
|
-
"simple_duckdb_store_batch_error",
|
|
392
|
-
error=str(e),
|
|
393
|
-
metric_count=len(metrics),
|
|
394
|
-
)
|
|
395
|
-
return False
|
|
396
|
-
|
|
397
|
-
async def store(self, metric: AccessLogPayload) -> bool:
|
|
398
|
-
"""Store single metric.
|
|
399
|
-
|
|
400
|
-
Args:
|
|
401
|
-
metric: Metric data to store
|
|
402
|
-
|
|
403
|
-
Returns:
|
|
404
|
-
True if stored successfully
|
|
405
|
-
"""
|
|
406
|
-
return await self.store_batch([metric])
|
|
407
|
-
|
|
408
|
-
async def query(
|
|
409
|
-
self,
|
|
410
|
-
sql: str,
|
|
411
|
-
params: dict[str, Any] | list[Any] | None = None,
|
|
412
|
-
limit: int = 1000,
|
|
413
|
-
) -> list[dict[str, Any]]:
|
|
414
|
-
"""Execute SQL query and return results.
|
|
415
|
-
|
|
416
|
-
Args:
|
|
417
|
-
sql: SQL query string
|
|
418
|
-
params: Query parameters
|
|
419
|
-
limit: Maximum number of results
|
|
420
|
-
|
|
421
|
-
Returns:
|
|
422
|
-
List of result rows as dictionaries
|
|
423
|
-
"""
|
|
424
|
-
if not self._initialized or not self._engine:
|
|
425
|
-
return []
|
|
426
|
-
|
|
427
|
-
try:
|
|
428
|
-
# Use SQLModel for querying
|
|
429
|
-
with Session(self._engine) as session:
|
|
430
|
-
# For now, we'll use raw SQL through the engine
|
|
431
|
-
# In a full implementation, this would be converted to SQLModel queries
|
|
432
|
-
|
|
433
|
-
# Use parameterized query to prevent SQL injection
|
|
434
|
-
limited_sql = "SELECT * FROM (" + sql + ") LIMIT :limit"
|
|
435
|
-
|
|
436
|
-
query_params = {"limit": limit}
|
|
437
|
-
if params:
|
|
438
|
-
# Merge user params with limit param
|
|
439
|
-
if isinstance(params, dict):
|
|
440
|
-
query_params.update(params)
|
|
441
|
-
result = session.execute(text(limited_sql), query_params)
|
|
442
|
-
else:
|
|
443
|
-
# If params is a list, we need to handle it differently
|
|
444
|
-
# For now, we'll use the safer approach of not supporting list params with limits
|
|
445
|
-
result = session.execute(text(sql), params)
|
|
446
|
-
else:
|
|
447
|
-
result = session.execute(text(limited_sql), query_params)
|
|
448
|
-
|
|
449
|
-
# Convert to list of dictionaries
|
|
450
|
-
columns = list(result.keys())
|
|
451
|
-
rows = result.fetchall()
|
|
452
|
-
|
|
453
|
-
return [dict(zip(columns, row, strict=False)) for row in rows]
|
|
454
|
-
|
|
455
|
-
except Exception as e:
|
|
456
|
-
logger.error("simple_duckdb_query_error", sql=sql, error=str(e))
|
|
457
|
-
return []
|
|
458
|
-
|
|
459
|
-
async def get_recent_requests(self, limit: int = 100) -> list[dict[str, Any]]:
|
|
460
|
-
"""Get recent requests for debugging/monitoring.
|
|
461
|
-
|
|
462
|
-
Args:
|
|
463
|
-
limit: Number of recent requests to return
|
|
464
|
-
|
|
465
|
-
Returns:
|
|
466
|
-
List of recent request records
|
|
467
|
-
"""
|
|
468
|
-
if not self._engine:
|
|
469
|
-
return []
|
|
470
|
-
|
|
471
|
-
try:
|
|
472
|
-
with Session(self._engine) as session:
|
|
473
|
-
statement = (
|
|
474
|
-
select(AccessLog).order_by(desc(AccessLog.timestamp)).limit(limit)
|
|
475
|
-
)
|
|
476
|
-
results = session.exec(statement).all()
|
|
477
|
-
return [log.dict() for log in results]
|
|
478
|
-
except Exception as e:
|
|
479
|
-
logger.error("sqlmodel_query_error", error=str(e))
|
|
480
|
-
return []
|
|
481
|
-
|
|
482
|
-
async def get_analytics(
|
|
483
|
-
self,
|
|
484
|
-
start_time: float | None = None,
|
|
485
|
-
end_time: float | None = None,
|
|
486
|
-
model: str | None = None,
|
|
487
|
-
service_type: str | None = None,
|
|
488
|
-
) -> dict[str, Any]:
|
|
489
|
-
"""Get analytics using SQLModel.
|
|
490
|
-
|
|
491
|
-
Args:
|
|
492
|
-
start_time: Start timestamp (Unix time)
|
|
493
|
-
end_time: End timestamp (Unix time)
|
|
494
|
-
model: Filter by model name
|
|
495
|
-
service_type: Filter by service type
|
|
496
|
-
|
|
497
|
-
Returns:
|
|
498
|
-
Analytics summary data
|
|
499
|
-
"""
|
|
500
|
-
if not self._engine:
|
|
501
|
-
return {}
|
|
502
|
-
|
|
503
|
-
try:
|
|
504
|
-
with Session(self._engine) as session:
|
|
505
|
-
# Build base query
|
|
506
|
-
statement = select(AccessLog)
|
|
507
|
-
|
|
508
|
-
# Add filters - convert Unix timestamps to datetime
|
|
509
|
-
if start_time:
|
|
510
|
-
start_dt = datetime.fromtimestamp(start_time)
|
|
511
|
-
statement = statement.where(AccessLog.timestamp >= start_dt)
|
|
512
|
-
if end_time:
|
|
513
|
-
end_dt = datetime.fromtimestamp(end_time)
|
|
514
|
-
statement = statement.where(AccessLog.timestamp <= end_dt)
|
|
515
|
-
if model:
|
|
516
|
-
statement = statement.where(AccessLog.model == model)
|
|
517
|
-
if service_type:
|
|
518
|
-
statement = statement.where(AccessLog.service_type == service_type)
|
|
519
|
-
|
|
520
|
-
# Get summary statistics using individual queries to avoid overload issues
|
|
521
|
-
base_where_conditions = []
|
|
522
|
-
if start_time:
|
|
523
|
-
start_dt = datetime.fromtimestamp(start_time)
|
|
524
|
-
base_where_conditions.append(AccessLog.timestamp >= start_dt)
|
|
525
|
-
if end_time:
|
|
526
|
-
end_dt = datetime.fromtimestamp(end_time)
|
|
527
|
-
base_where_conditions.append(AccessLog.timestamp <= end_dt)
|
|
528
|
-
if model:
|
|
529
|
-
base_where_conditions.append(AccessLog.model == model)
|
|
530
|
-
if service_type:
|
|
531
|
-
base_where_conditions.append(AccessLog.service_type == service_type)
|
|
532
|
-
|
|
533
|
-
total_requests = session.exec(
|
|
534
|
-
select(func.count())
|
|
535
|
-
.select_from(AccessLog)
|
|
536
|
-
.where(*base_where_conditions)
|
|
537
|
-
).first()
|
|
538
|
-
|
|
539
|
-
avg_duration = session.exec(
|
|
540
|
-
select(func.avg(AccessLog.duration_ms))
|
|
541
|
-
.select_from(AccessLog)
|
|
542
|
-
.where(*base_where_conditions)
|
|
543
|
-
).first()
|
|
544
|
-
|
|
545
|
-
total_cost = session.exec(
|
|
546
|
-
select(func.sum(AccessLog.cost_usd))
|
|
547
|
-
.select_from(AccessLog)
|
|
548
|
-
.where(*base_where_conditions)
|
|
549
|
-
).first()
|
|
550
|
-
|
|
551
|
-
total_tokens_input = session.exec(
|
|
552
|
-
select(func.sum(AccessLog.tokens_input))
|
|
553
|
-
.select_from(AccessLog)
|
|
554
|
-
.where(*base_where_conditions)
|
|
555
|
-
).first()
|
|
556
|
-
|
|
557
|
-
total_tokens_output = session.exec(
|
|
558
|
-
select(func.sum(AccessLog.tokens_output))
|
|
559
|
-
.select_from(AccessLog)
|
|
560
|
-
.where(*base_where_conditions)
|
|
561
|
-
).first()
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
"summary": {
|
|
565
|
-
"total_requests": total_requests or 0,
|
|
566
|
-
"avg_duration_ms": avg_duration or 0,
|
|
567
|
-
"total_cost_usd": total_cost or 0,
|
|
568
|
-
"total_tokens_input": total_tokens_input or 0,
|
|
569
|
-
"total_tokens_output": total_tokens_output or 0,
|
|
570
|
-
},
|
|
571
|
-
"query_time": time.time(),
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
except Exception as e:
|
|
575
|
-
logger.error("sqlmodel_analytics_error", error=str(e))
|
|
576
|
-
return {}
|
|
577
|
-
|
|
578
|
-
async def close(self) -> None:
|
|
579
|
-
"""Close the database connection and stop background worker."""
|
|
580
|
-
# Signal shutdown to background worker
|
|
581
|
-
self._shutdown_event.set()
|
|
582
|
-
|
|
583
|
-
# Wait for background worker to finish
|
|
584
|
-
if self._background_worker_task:
|
|
585
|
-
try:
|
|
586
|
-
await asyncio.wait_for(self._background_worker_task, timeout=5.0)
|
|
587
|
-
except TimeoutError:
|
|
588
|
-
logger.warning("background_worker_shutdown_timeout")
|
|
589
|
-
self._background_worker_task.cancel()
|
|
590
|
-
except Exception as e:
|
|
591
|
-
logger.error("background_worker_shutdown_error", error=str(e))
|
|
592
|
-
|
|
593
|
-
# Process remaining items in queue (with timeout)
|
|
594
|
-
try:
|
|
595
|
-
await asyncio.wait_for(self._write_queue.join(), timeout=2.0)
|
|
596
|
-
except TimeoutError:
|
|
597
|
-
logger.warning(
|
|
598
|
-
"queue_drain_timeout", remaining_items=self._write_queue.qsize()
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
if self._engine:
|
|
602
|
-
try:
|
|
603
|
-
self._engine.dispose()
|
|
604
|
-
except Exception as e:
|
|
605
|
-
logger.error("simple_duckdb_engine_close_error", error=str(e))
|
|
606
|
-
finally:
|
|
607
|
-
self._engine = None
|
|
608
|
-
|
|
609
|
-
self._initialized = False
|
|
610
|
-
|
|
611
|
-
def is_enabled(self) -> bool:
|
|
612
|
-
"""Check if storage is enabled and available."""
|
|
613
|
-
return self._initialized
|
|
614
|
-
|
|
615
|
-
async def health_check(self) -> dict[str, Any]:
|
|
616
|
-
"""Get health status of the storage backend."""
|
|
617
|
-
if not self._initialized:
|
|
618
|
-
return {
|
|
619
|
-
"status": "not_initialized",
|
|
620
|
-
"enabled": False,
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
try:
|
|
624
|
-
if self._engine:
|
|
625
|
-
with Session(self._engine) as session:
|
|
626
|
-
statement = select(func.count()).select_from(AccessLog)
|
|
627
|
-
access_log_count = session.exec(statement).first()
|
|
628
|
-
|
|
629
|
-
return {
|
|
630
|
-
"status": "healthy",
|
|
631
|
-
"enabled": True,
|
|
632
|
-
"database_path": str(self.database_path),
|
|
633
|
-
"access_log_count": access_log_count,
|
|
634
|
-
"backend": "sqlmodel",
|
|
635
|
-
}
|
|
636
|
-
else:
|
|
637
|
-
return {
|
|
638
|
-
"status": "no_connection",
|
|
639
|
-
"enabled": False,
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
except Exception as e:
|
|
643
|
-
return {
|
|
644
|
-
"status": "unhealthy",
|
|
645
|
-
"enabled": False,
|
|
646
|
-
"error": str(e),
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async def reset_data(self) -> bool:
|
|
650
|
-
"""Reset all data in the storage (useful for testing/debugging).
|
|
651
|
-
|
|
652
|
-
Returns:
|
|
653
|
-
True if reset was successful
|
|
654
|
-
"""
|
|
655
|
-
if not self._initialized or not self._engine:
|
|
656
|
-
return False
|
|
657
|
-
|
|
658
|
-
try:
|
|
659
|
-
# Run the reset operation in a thread pool
|
|
660
|
-
return await asyncio.to_thread(self._reset_data_sync)
|
|
661
|
-
except Exception as e:
|
|
662
|
-
logger.error("simple_duckdb_reset_error", error=str(e))
|
|
663
|
-
return False
|
|
664
|
-
|
|
665
|
-
def _reset_data_sync(self) -> bool:
|
|
666
|
-
"""Synchronous version of reset_data for thread pool execution."""
|
|
667
|
-
try:
|
|
668
|
-
with Session(self._engine) as session:
|
|
669
|
-
# Delete all records from access_logs table
|
|
670
|
-
session.execute(text("DELETE FROM access_logs"))
|
|
671
|
-
session.commit()
|
|
672
|
-
|
|
673
|
-
logger.info("simple_duckdb_reset_success")
|
|
674
|
-
return True
|
|
675
|
-
except Exception as e:
|
|
676
|
-
logger.error("simple_duckdb_reset_sync_error", error=str(e))
|
|
677
|
-
return False
|